@nanahoshi/mona-ui 0.13.1 → 0.14.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.
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { computed, input, Component, output, inject, ApplicationRef, DOCUMENT, Injector, RendererFactory2, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import { LucideLoader, LucideDynamicIcon } from '@lucide/angular';
|
|
4
|
+
import { classInputToClass } from '@nanahoshi/mona-ui/common';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import { cva } from 'class-variance-authority';
|
|
7
|
+
import { Subject, defer, finalize } from 'rxjs';
|
|
8
|
+
import { v4 } from 'uuid';
|
|
9
|
+
|
|
10
|
+
const spinnerThemeVariants = cva("inline-flex items-center justify-center flex-none aspect-square relative text-current select-none", {
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
small: "w-3 h-3",
|
|
14
|
+
medium: "w-4 h-4",
|
|
15
|
+
large: "w-6 h-6"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
size: "medium"
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @description
|
|
25
|
+
* An indeterminate loading indicator primitive for communicating ongoing activity.
|
|
26
|
+
* Supports multiple appearances, standard sizes, and accessible status announcements.
|
|
27
|
+
*/
|
|
28
|
+
class SpinnerComponent {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.baseClass = computed(() => {
|
|
31
|
+
const variantClass = spinnerThemeVariants({ size: this.size() });
|
|
32
|
+
return twMerge(variantClass, this.userClass());
|
|
33
|
+
}, /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
35
|
+
this.loaderIcon = LucideLoader;
|
|
36
|
+
this.pixelSize = computed(() => {
|
|
37
|
+
switch (this.size()) {
|
|
38
|
+
case "small":
|
|
39
|
+
return 12;
|
|
40
|
+
case "large":
|
|
41
|
+
return 24;
|
|
42
|
+
case "medium":
|
|
43
|
+
default:
|
|
44
|
+
return 16;
|
|
45
|
+
}
|
|
46
|
+
}, /* @ts-ignore */
|
|
47
|
+
...(ngDevMode ? [{ debugName: "pixelSize" }] : /* istanbul ignore next */ []));
|
|
48
|
+
/**
|
|
49
|
+
* @description Visual appearance of the spinner animation.
|
|
50
|
+
* @default "default"
|
|
51
|
+
*/
|
|
52
|
+
this.appearance = input("default", /* @ts-ignore */
|
|
53
|
+
...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
|
|
54
|
+
/**
|
|
55
|
+
* @description Accessible text announced by assistive technology when the spinner is not decorative.
|
|
56
|
+
* @default "Loading"
|
|
57
|
+
*/
|
|
58
|
+
this.ariaLabel = input("Loading", { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: "aria-label" });
|
|
59
|
+
/**
|
|
60
|
+
* @description When `true`, removes the `status` role and hides the spinner from assistive technology with `aria-hidden="true"`.
|
|
61
|
+
* Useful when a parent element already manages loading semantics (such as `aria-busy="true"` on a button).
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
this.decorative = input(false, /* @ts-ignore */
|
|
65
|
+
...(ngDevMode ? [{ debugName: "decorative" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/**
|
|
67
|
+
* @description Size preset controlling the dimensions of the indicator.
|
|
68
|
+
* @default "medium"
|
|
69
|
+
*/
|
|
70
|
+
this.size = input("medium", /* @ts-ignore */
|
|
71
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
72
|
+
/**
|
|
73
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
74
|
+
* @default ""
|
|
75
|
+
*/
|
|
76
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
77
|
+
transform: value => classInputToClass(value) });
|
|
78
|
+
}
|
|
79
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
80
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SpinnerComponent, isStandalone: true, selector: "mona-spinner", inputs: { appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "decorative() ? 'true' : null", "attr.aria-label": "decorative() ? null : (ariaLabel() || 'Loading')", "attr.role": "decorative() ? null : 'status'", "class": "baseClass()" } }, ngImport: i0, template: "@switch (appearance()) {\n @case (\"default\") {\n <svg [lucideIcon]=\"loaderIcon\" [size]=\"pixelSize()\" class=\"animate-[spin_1.5s_linear_infinite] motion-reduce:animate-none\"></svg>\n }\n @case (\"pulsing\") {\n <span class=\"mona-spinner-pulsing-container\">\n <span data-segment class=\"mona-spinner-pulsing-segment mona-spinner-pulsing-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-segment mona-spinner-pulsing-2\"></span>\n </span>\n }\n @case (\"pulsing-triad\") {\n <span class=\"mona-spinner-pulsing-triad-container\">\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-2\"></span>\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-3\"></span>\n </span>\n }\n @case (\"pulsing-ring\") {\n <span class=\"mona-spinner-pulsing-ring-container\">\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-2\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-3\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-4\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-5\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-6\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-7\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-8\"></span>\n </span>\n }\n @case (\"converging-spinner\") {\n <span class=\"mona-spinner-converge-container\">\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-1\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-2\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-3\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-4\"></span>\n </span>\n }\n}\n", styles: [":host{display:inline-flex;vertical-align:middle}.mona-spinner-pulsing-container{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.mona-spinner-pulsing-segment{position:absolute;inset:0;border-radius:9999px;background-color:currentColor;opacity:.6;animation:mona-spinner-pulse 1.2s cubic-bezier(0,.2,.8,1) infinite}.mona-spinner-pulsing-2{animation-delay:-.6s}@keyframes mona-spinner-pulse{0%{transform:scale(.1);opacity:.9}50%{transform:scale(.6);opacity:.5}to{transform:scale(1);opacity:0}}.mona-spinner-pulsing-triad-container{position:relative;width:100%;height:100%}.mona-spinner-pulsing-triad-segment{position:absolute;width:32%;height:32%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-dot-pulse 1.4s ease-in-out infinite}.mona-spinner-pulsing-triad-1{top:0;left:34%;animation-delay:-.933s}.mona-spinner-pulsing-triad-2{bottom:0;left:0;animation-delay:-.467s}.mona-spinner-pulsing-triad-3{bottom:0;right:0;animation-delay:0s}@keyframes mona-spinner-dot-pulse{0%,to{transform:scale(.4);opacity:.4}50%{transform:scale(1);opacity:1}}.mona-spinner-pulsing-ring-container{position:relative;width:100%;height:100%}.mona-spinner-pulsing-ring-segment{position:absolute;width:22%;height:22%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-dot-pulse 1.6s ease-in-out infinite}.mona-spinner-pulsing-ring-1{top:5%;left:39%;animation-delay:0s}.mona-spinner-pulsing-ring-2{top:14.96%;left:63.04%;animation-delay:-.2s}.mona-spinner-pulsing-ring-3{top:39%;left:73%;animation-delay:-.4s}.mona-spinner-pulsing-ring-4{top:63.04%;left:63.04%;animation-delay:-.6s}.mona-spinner-pulsing-ring-5{top:73%;left:39%;animation-delay:-.8s}.mona-spinner-pulsing-ring-6{top:63.04%;left:14.96%;animation-delay:-1s}.mona-spinner-pulsing-ring-7{top:39%;left:5%;animation-delay:-1.2s}.mona-spinner-pulsing-ring-8{top:14.96%;left:14.96%;animation-delay:-1.4s}.mona-spinner-converge-container{position:relative;width:100%;height:100%;animation:mona-spinner-rotate 1.5s linear infinite}.mona-spinner-converge-segment{position:absolute;width:32%;height:32%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-converge 1.5s ease-in-out infinite}.mona-spinner-converge-1{top:0;left:0}.mona-spinner-converge-2{top:0;right:0}.mona-spinner-converge-3{bottom:0;right:0}.mona-spinner-converge-4{bottom:0;left:0}@keyframes mona-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes mona-spinner-converge{0%,to{transform:scale(1)}50%{transform:scale(.4)}}@media(prefers-reduced-motion:reduce){.mona-spinner-pulsing-segment,.mona-spinner-pulsing-triad-segment,.mona-spinner-pulsing-ring-segment,.mona-spinner-converge-container,.mona-spinner-converge-segment{animation:none!important}.mona-spinner-pulsing-segment{opacity:.6;transform:scale(.85)}.mona-spinner-pulsing-2{opacity:.3;transform:scale(.5)}.mona-spinner-pulsing-triad-segment,.mona-spinner-pulsing-ring-segment{opacity:.7;transform:scale(.8)}}\n"], dependencies: [{ kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] }); }
|
|
81
|
+
}
|
|
82
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerComponent, decorators: [{
|
|
83
|
+
type: Component,
|
|
84
|
+
args: [{ selector: "mona-spinner", imports: [LucideDynamicIcon], host: {
|
|
85
|
+
"[attr.aria-hidden]": "decorative() ? 'true' : null",
|
|
86
|
+
"[attr.aria-label]": "decorative() ? null : (ariaLabel() || 'Loading')",
|
|
87
|
+
"[attr.role]": "decorative() ? null : 'status'",
|
|
88
|
+
"[class]": "baseClass()"
|
|
89
|
+
}, template: "@switch (appearance()) {\n @case (\"default\") {\n <svg [lucideIcon]=\"loaderIcon\" [size]=\"pixelSize()\" class=\"animate-[spin_1.5s_linear_infinite] motion-reduce:animate-none\"></svg>\n }\n @case (\"pulsing\") {\n <span class=\"mona-spinner-pulsing-container\">\n <span data-segment class=\"mona-spinner-pulsing-segment mona-spinner-pulsing-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-segment mona-spinner-pulsing-2\"></span>\n </span>\n }\n @case (\"pulsing-triad\") {\n <span class=\"mona-spinner-pulsing-triad-container\">\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-2\"></span>\n <span data-segment class=\"mona-spinner-pulsing-triad-segment mona-spinner-pulsing-triad-3\"></span>\n </span>\n }\n @case (\"pulsing-ring\") {\n <span class=\"mona-spinner-pulsing-ring-container\">\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-1\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-2\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-3\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-4\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-5\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-6\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-7\"></span>\n <span data-segment class=\"mona-spinner-pulsing-ring-segment mona-spinner-pulsing-ring-8\"></span>\n </span>\n }\n @case (\"converging-spinner\") {\n <span class=\"mona-spinner-converge-container\">\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-1\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-2\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-3\"></span>\n <span data-segment class=\"mona-spinner-converge-segment mona-spinner-converge-4\"></span>\n </span>\n }\n}\n", styles: [":host{display:inline-flex;vertical-align:middle}.mona-spinner-pulsing-container{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.mona-spinner-pulsing-segment{position:absolute;inset:0;border-radius:9999px;background-color:currentColor;opacity:.6;animation:mona-spinner-pulse 1.2s cubic-bezier(0,.2,.8,1) infinite}.mona-spinner-pulsing-2{animation-delay:-.6s}@keyframes mona-spinner-pulse{0%{transform:scale(.1);opacity:.9}50%{transform:scale(.6);opacity:.5}to{transform:scale(1);opacity:0}}.mona-spinner-pulsing-triad-container{position:relative;width:100%;height:100%}.mona-spinner-pulsing-triad-segment{position:absolute;width:32%;height:32%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-dot-pulse 1.4s ease-in-out infinite}.mona-spinner-pulsing-triad-1{top:0;left:34%;animation-delay:-.933s}.mona-spinner-pulsing-triad-2{bottom:0;left:0;animation-delay:-.467s}.mona-spinner-pulsing-triad-3{bottom:0;right:0;animation-delay:0s}@keyframes mona-spinner-dot-pulse{0%,to{transform:scale(.4);opacity:.4}50%{transform:scale(1);opacity:1}}.mona-spinner-pulsing-ring-container{position:relative;width:100%;height:100%}.mona-spinner-pulsing-ring-segment{position:absolute;width:22%;height:22%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-dot-pulse 1.6s ease-in-out infinite}.mona-spinner-pulsing-ring-1{top:5%;left:39%;animation-delay:0s}.mona-spinner-pulsing-ring-2{top:14.96%;left:63.04%;animation-delay:-.2s}.mona-spinner-pulsing-ring-3{top:39%;left:73%;animation-delay:-.4s}.mona-spinner-pulsing-ring-4{top:63.04%;left:63.04%;animation-delay:-.6s}.mona-spinner-pulsing-ring-5{top:73%;left:39%;animation-delay:-.8s}.mona-spinner-pulsing-ring-6{top:63.04%;left:14.96%;animation-delay:-1s}.mona-spinner-pulsing-ring-7{top:39%;left:5%;animation-delay:-1.2s}.mona-spinner-pulsing-ring-8{top:14.96%;left:14.96%;animation-delay:-1.4s}.mona-spinner-converge-container{position:relative;width:100%;height:100%;animation:mona-spinner-rotate 1.5s linear infinite}.mona-spinner-converge-segment{position:absolute;width:32%;height:32%;border-radius:9999px;background-color:currentColor;animation:mona-spinner-converge 1.5s ease-in-out infinite}.mona-spinner-converge-1{top:0;left:0}.mona-spinner-converge-2{top:0;right:0}.mona-spinner-converge-3{bottom:0;right:0}.mona-spinner-converge-4{bottom:0;left:0}@keyframes mona-spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes mona-spinner-converge{0%,to{transform:scale(1)}50%{transform:scale(.4)}}@media(prefers-reduced-motion:reduce){.mona-spinner-pulsing-segment,.mona-spinner-pulsing-triad-segment,.mona-spinner-pulsing-ring-segment,.mona-spinner-converge-container,.mona-spinner-converge-segment{animation:none!important}.mona-spinner-pulsing-segment{opacity:.6;transform:scale(.85)}.mona-spinner-pulsing-2{opacity:.3;transform:scale(.5)}.mona-spinner-pulsing-triad-segment,.mona-spinner-pulsing-ring-segment{opacity:.7;transform:scale(.8)}}\n"] }]
|
|
90
|
+
}], propDecorators: { appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], decorative: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorative", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
91
|
+
|
|
92
|
+
const spinnerOverlayThemeVariants = cva(`
|
|
93
|
+
isolate flex items-center justify-center
|
|
94
|
+
pointer-events-auto select-none
|
|
95
|
+
bg-background/60 backdrop-blur-xs
|
|
96
|
+
text-foreground
|
|
97
|
+
`, {
|
|
98
|
+
variants: {
|
|
99
|
+
fullPage: {
|
|
100
|
+
true: "fixed inset-0 z-50",
|
|
101
|
+
false: "absolute inset-0 z-1"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
fullPage: false
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @description
|
|
111
|
+
* Internal overlay component managed exclusively by `SpinnerService`.
|
|
112
|
+
* Hosts a decorative `SpinnerComponent`, status message, and optional cancellation control.
|
|
113
|
+
*/
|
|
114
|
+
class SpinnerOverlayComponent {
|
|
115
|
+
constructor() {
|
|
116
|
+
this.baseClass = computed(() => {
|
|
117
|
+
return spinnerOverlayThemeVariants({ fullPage: this.fullPage() });
|
|
118
|
+
}, /* @ts-ignore */
|
|
119
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
120
|
+
this.effectiveZIndex = computed(() => {
|
|
121
|
+
const customZIndex = this.zIndex();
|
|
122
|
+
if (customZIndex !== undefined) {
|
|
123
|
+
return customZIndex;
|
|
124
|
+
}
|
|
125
|
+
return this.fullPage() ? 50 : 1;
|
|
126
|
+
}, /* @ts-ignore */
|
|
127
|
+
...(ngDevMode ? [{ debugName: "effectiveZIndex" }] : /* istanbul ignore next */ []));
|
|
128
|
+
/**
|
|
129
|
+
* @description Visual appearance of the nested spinner animation.
|
|
130
|
+
* @default "default"
|
|
131
|
+
*/
|
|
132
|
+
this.appearance = input("default", /* @ts-ignore */
|
|
133
|
+
...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
|
|
134
|
+
/**
|
|
135
|
+
* @description Emits when the consumer clicks the cancellation action button.
|
|
136
|
+
*/
|
|
137
|
+
this.cancel = output();
|
|
138
|
+
/**
|
|
139
|
+
* @description Whether the cancellation button is displayed in the overlay.
|
|
140
|
+
* @default false
|
|
141
|
+
*/
|
|
142
|
+
this.cancellable = input(false, /* @ts-ignore */
|
|
143
|
+
...(ngDevMode ? [{ debugName: "cancellable" }] : /* istanbul ignore next */ []));
|
|
144
|
+
/**
|
|
145
|
+
* @description Label text displayed inside the cancellation button.
|
|
146
|
+
* @default "Cancel"
|
|
147
|
+
*/
|
|
148
|
+
this.cancelText = input("Cancel", /* @ts-ignore */
|
|
149
|
+
...(ngDevMode ? [{ debugName: "cancelText" }] : /* istanbul ignore next */ []));
|
|
150
|
+
/**
|
|
151
|
+
* @description Whether the overlay is fixed across the viewport or absolute within a target container.
|
|
152
|
+
* @default false
|
|
153
|
+
*/
|
|
154
|
+
this.fullPage = input(false, /* @ts-ignore */
|
|
155
|
+
...(ngDevMode ? [{ debugName: "fullPage" }] : /* istanbul ignore next */ []));
|
|
156
|
+
/**
|
|
157
|
+
* @description Size preset passed to the nested spinner.
|
|
158
|
+
* @default "medium"
|
|
159
|
+
*/
|
|
160
|
+
this.size = input("medium", /* @ts-ignore */
|
|
161
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
162
|
+
/**
|
|
163
|
+
* @description Status text displayed below the spinner indicator.
|
|
164
|
+
* @default undefined
|
|
165
|
+
*/
|
|
166
|
+
this.text = input(undefined, /* @ts-ignore */
|
|
167
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
168
|
+
/**
|
|
169
|
+
* @description Custom z-index value for unusual stacking contexts.
|
|
170
|
+
* @default undefined
|
|
171
|
+
*/
|
|
172
|
+
this.zIndex = input(undefined, /* @ts-ignore */
|
|
173
|
+
...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
|
|
174
|
+
}
|
|
175
|
+
onCancel() {
|
|
176
|
+
this.cancel.emit();
|
|
177
|
+
}
|
|
178
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
179
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SpinnerOverlayComponent, isStandalone: true, selector: "mona-spinner-overlay", inputs: { appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, cancellable: { classPropertyName: "cancellable", publicName: "cancellable", isSignal: true, isRequired: false, transformFunction: null }, cancelText: { classPropertyName: "cancelText", publicName: "cancelText", isSignal: true, isRequired: false, transformFunction: null }, fullPage: { classPropertyName: "fullPage", publicName: "fullPage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cancel: "cancel" }, host: { properties: { "class": "baseClass()", "style.z-index": "effectiveZIndex()" } }, ngImport: i0, template: "<div class=\"flex flex-col items-center justify-center gap-2.5 p-4 text-sm font-medium text-foreground text-center\" role=\"status\" aria-live=\"polite\">\n <mona-spinner [appearance]=\"appearance()\" [size]=\"size()\" [decorative]=\"true\"></mona-spinner>\n\n @if (text()) {\n <span class=\"max-w-xs truncate\">{{ text() }}</span>\n } @else {\n <span class=\"sr-only\">Loading</span>\n }\n\n @if (cancellable()) {\n <button\n type=\"button\"\n (click)=\"onCancel()\"\n class=\"mt-1 inline-flex items-center justify-center rounded-md border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer transition-colors\"\n >\n {{ cancelText() }}\n </button>\n }\n</div>\n", dependencies: [{ kind: "component", type: SpinnerComponent, selector: "mona-spinner", inputs: ["appearance", "aria-label", "decorative", "size", "class"] }] }); }
|
|
180
|
+
}
|
|
181
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerOverlayComponent, decorators: [{
|
|
182
|
+
type: Component,
|
|
183
|
+
args: [{ selector: "mona-spinner-overlay", imports: [SpinnerComponent], host: {
|
|
184
|
+
"[class]": "baseClass()",
|
|
185
|
+
"[style.z-index]": "effectiveZIndex()"
|
|
186
|
+
}, template: "<div class=\"flex flex-col items-center justify-center gap-2.5 p-4 text-sm font-medium text-foreground text-center\" role=\"status\" aria-live=\"polite\">\n <mona-spinner [appearance]=\"appearance()\" [size]=\"size()\" [decorative]=\"true\"></mona-spinner>\n\n @if (text()) {\n <span class=\"max-w-xs truncate\">{{ text() }}</span>\n } @else {\n <span class=\"sr-only\">Loading</span>\n }\n\n @if (cancellable()) {\n <button\n type=\"button\"\n (click)=\"onCancel()\"\n class=\"mt-1 inline-flex items-center justify-center rounded-md border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer transition-colors\"\n >\n {{ cancelText() }}\n </button>\n }\n</div>\n" }]
|
|
187
|
+
}], propDecorators: { appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], cancel: [{ type: i0.Output, args: ["cancel"] }], cancellable: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancellable", required: false }] }], cancelText: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelText", required: false }] }], fullPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullPage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }] } });
|
|
188
|
+
|
|
189
|
+
class SpinnerService {
|
|
190
|
+
#appRef = inject(ApplicationRef);
|
|
191
|
+
#document = inject(DOCUMENT);
|
|
192
|
+
#injector = inject(Injector);
|
|
193
|
+
#renderer = inject(RendererFactory2).createRenderer(null, null);
|
|
194
|
+
#requests = new Map();
|
|
195
|
+
#targetStates = new Set();
|
|
196
|
+
#targets = new WeakMap();
|
|
197
|
+
/**
|
|
198
|
+
* Hides and destroys the spinner associated with the given `SpinnerRef` or string `id`.
|
|
199
|
+
*/
|
|
200
|
+
hide(refOrId) {
|
|
201
|
+
const id = typeof refOrId === "string" ? refOrId : refOrId.id;
|
|
202
|
+
this.#closeRequest(id);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Tears down every active/pending spinner request and physical overlay so
|
|
206
|
+
* nothing is left behind if the service outlives its consumers.
|
|
207
|
+
*/
|
|
208
|
+
ngOnDestroy() {
|
|
209
|
+
for (const request of this.#requests.values()) {
|
|
210
|
+
if (request.delayTimer != null) {
|
|
211
|
+
clearTimeout(request.delayTimer);
|
|
212
|
+
request.delayTimer = null;
|
|
213
|
+
}
|
|
214
|
+
if (!request.cancellationHandled) {
|
|
215
|
+
request.cancelledSubject.complete();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.#requests.clear();
|
|
219
|
+
for (const targetState of this.#targetStates) {
|
|
220
|
+
if (targetState.minDurationTimer != null) {
|
|
221
|
+
clearTimeout(targetState.minDurationTimer);
|
|
222
|
+
targetState.minDurationTimer = null;
|
|
223
|
+
}
|
|
224
|
+
targetState.cancelSubscription?.unsubscribe();
|
|
225
|
+
targetState.cancelSubscription = null;
|
|
226
|
+
if (targetState.componentRef != null) {
|
|
227
|
+
this.#appRef.detachView(targetState.componentRef.hostView);
|
|
228
|
+
targetState.componentRef.destroy();
|
|
229
|
+
targetState.componentRef = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.#targetStates.clear();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Displays a spinner loading overlay according to the provided options.
|
|
236
|
+
* Returns a `SpinnerRef` handle for lifecycle management and updates.
|
|
237
|
+
*/
|
|
238
|
+
show(options) {
|
|
239
|
+
const id = options?.id ?? `spinner-${v4()}`;
|
|
240
|
+
// If an existing request with the same ID exists, close it cleanly first
|
|
241
|
+
if (this.#requests.has(id)) {
|
|
242
|
+
this.#closeRequest(id);
|
|
243
|
+
}
|
|
244
|
+
const targets = this.#normalizeTargets(options?.target);
|
|
245
|
+
const isFullPage = !options?.target || targets.length === 0;
|
|
246
|
+
const resolvedTargets = isFullPage ? [this.#document.body] : targets;
|
|
247
|
+
const cancellableOpt = options?.cancellable;
|
|
248
|
+
const isCancellable = !!cancellableOpt;
|
|
249
|
+
const cancelText = typeof cancellableOpt === "object" && cancellableOpt.text ? cancellableOpt.text : "Cancel";
|
|
250
|
+
const onCancel = typeof cancellableOpt === "object" ? cancellableOpt.onCancel : undefined;
|
|
251
|
+
const cancelledSubject = new Subject();
|
|
252
|
+
const request = {
|
|
253
|
+
activatedAt: Date.now(),
|
|
254
|
+
appearance: options?.appearance ?? "default",
|
|
255
|
+
cancellationHandled: false,
|
|
256
|
+
cancellable: isCancellable,
|
|
257
|
+
cancelText,
|
|
258
|
+
delayTimer: null,
|
|
259
|
+
id,
|
|
260
|
+
isFullPage,
|
|
261
|
+
onCancel,
|
|
262
|
+
options: options ?? {},
|
|
263
|
+
registeredAt: Date.now(),
|
|
264
|
+
size: options?.size ?? "medium",
|
|
265
|
+
status: "PENDING",
|
|
266
|
+
cancelledSubject,
|
|
267
|
+
targets: resolvedTargets,
|
|
268
|
+
text: options?.text
|
|
269
|
+
};
|
|
270
|
+
this.#requests.set(id, request);
|
|
271
|
+
// Register request against all targets and set aria-busy. Targets that already
|
|
272
|
+
// have a visible overlay activate immediately; idle targets respect `delay`.
|
|
273
|
+
const immediateTargets = [];
|
|
274
|
+
const delayedTargets = [];
|
|
275
|
+
for (const target of resolvedTargets) {
|
|
276
|
+
const targetState = this.#getOrCreateTargetState(target, isFullPage);
|
|
277
|
+
targetState.pendingRequestIds.add(id);
|
|
278
|
+
if (targetState.componentRef != null) {
|
|
279
|
+
immediateTargets.push(target);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
delayedTargets.push(target);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (immediateTargets.length > 0) {
|
|
286
|
+
this.#activateRequestOnTargets(request, immediateTargets);
|
|
287
|
+
}
|
|
288
|
+
const delay = options?.delay ?? 0;
|
|
289
|
+
if (delayedTargets.length > 0) {
|
|
290
|
+
if (delay > 0) {
|
|
291
|
+
request.delayTimer = setTimeout(() => {
|
|
292
|
+
request.delayTimer = null;
|
|
293
|
+
if (request.status !== "CLOSED") {
|
|
294
|
+
this.#activateRequestOnTargets(request, delayedTargets);
|
|
295
|
+
}
|
|
296
|
+
}, delay);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
this.#activateRequestOnTargets(request, delayedTargets);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
id,
|
|
304
|
+
cancelled$: cancelledSubject.asObservable(),
|
|
305
|
+
close: () => this.#closeRequest(id),
|
|
306
|
+
update: update => this.#updateRequest(id, update)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Wraps an Observable operation with a Spinner lifecycle.
|
|
311
|
+
* Shows the spinner on subscription and closes it when the observable completes, errors, or is unsubscribed.
|
|
312
|
+
*/
|
|
313
|
+
track(source, options) {
|
|
314
|
+
return defer(() => {
|
|
315
|
+
const ref = this.show(options);
|
|
316
|
+
return source.pipe(finalize(() => ref.close()));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
#activateRequestOnTargets(request, targets) {
|
|
320
|
+
if (request.status === "CLOSED") {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
request.status = "VISIBLE";
|
|
324
|
+
request.activatedAt = Date.now();
|
|
325
|
+
for (const target of targets) {
|
|
326
|
+
const targetState = this.#getOrCreateTargetState(target, request.isFullPage);
|
|
327
|
+
targetState.activeRequestIds.add(request.id);
|
|
328
|
+
this.#updateTargetOverlay(targetState);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
#cancelRequest(requestId) {
|
|
332
|
+
const request = this.#requests.get(requestId);
|
|
333
|
+
if (!request || request.cancellationHandled || request.status === "CLOSED") {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
request.cancellationHandled = true;
|
|
337
|
+
request.cancelledSubject.next();
|
|
338
|
+
request.cancelledSubject.complete();
|
|
339
|
+
try {
|
|
340
|
+
if (request.onCancel) {
|
|
341
|
+
request.onCancel();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
this.#closeRequest(requestId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
#closeRequest(requestId) {
|
|
349
|
+
const request = this.#requests.get(requestId);
|
|
350
|
+
if (!request || request.status === "CLOSED") {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
request.status = "CLOSED";
|
|
354
|
+
if (request.delayTimer != null) {
|
|
355
|
+
clearTimeout(request.delayTimer);
|
|
356
|
+
request.delayTimer = null;
|
|
357
|
+
}
|
|
358
|
+
if (!request.cancellationHandled) {
|
|
359
|
+
request.cancelledSubject.complete();
|
|
360
|
+
}
|
|
361
|
+
this.#requests.delete(requestId);
|
|
362
|
+
for (const target of request.targets) {
|
|
363
|
+
const targetState = this.#targets.get(target);
|
|
364
|
+
if (!targetState) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
targetState.pendingRequestIds.delete(requestId);
|
|
368
|
+
targetState.activeRequestIds.delete(requestId);
|
|
369
|
+
if (targetState.activeRequestIds.size > 0) {
|
|
370
|
+
// Other active requests remain; recompute winning presentation
|
|
371
|
+
this.#updateTargetOverlay(targetState);
|
|
372
|
+
}
|
|
373
|
+
else if (targetState.pendingRequestIds.size === 0) {
|
|
374
|
+
// No active or pending requests remain on this target
|
|
375
|
+
const minDuration = request.options.minimumVisibleDuration ?? 0;
|
|
376
|
+
if (minDuration > 0 && targetState.visibleSince != null && targetState.componentRef != null) {
|
|
377
|
+
const elapsed = Date.now() - targetState.visibleSince;
|
|
378
|
+
const remaining = minDuration - elapsed;
|
|
379
|
+
if (remaining > 0) {
|
|
380
|
+
if (targetState.minDurationTimer != null) {
|
|
381
|
+
clearTimeout(targetState.minDurationTimer);
|
|
382
|
+
}
|
|
383
|
+
targetState.minDurationTimer = setTimeout(() => {
|
|
384
|
+
targetState.minDurationTimer = null;
|
|
385
|
+
if (targetState.activeRequestIds.size === 0 && targetState.pendingRequestIds.size === 0) {
|
|
386
|
+
this.#teardownTarget(targetState);
|
|
387
|
+
}
|
|
388
|
+
}, remaining);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.#teardownTarget(targetState);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.#teardownTarget(targetState);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
#getOrCreateTargetState(target, isFullPage) {
|
|
401
|
+
let state = this.#targets.get(target);
|
|
402
|
+
if (!state) {
|
|
403
|
+
const originalAriaBusy = target.getAttribute("aria-busy");
|
|
404
|
+
const originalInlinePosition = target.style.position;
|
|
405
|
+
state = {
|
|
406
|
+
activeRequestIds: new Set(),
|
|
407
|
+
cancelSubscription: null,
|
|
408
|
+
changedPosition: false,
|
|
409
|
+
componentRef: null,
|
|
410
|
+
isFullPage,
|
|
411
|
+
minDurationTimer: null,
|
|
412
|
+
originalAriaBusy,
|
|
413
|
+
originalInlinePosition,
|
|
414
|
+
pendingRequestIds: new Set(),
|
|
415
|
+
target
|
|
416
|
+
};
|
|
417
|
+
this.#targets.set(target, state);
|
|
418
|
+
this.#targetStates.add(state);
|
|
419
|
+
}
|
|
420
|
+
// Apply aria-busy immediately while logically busy
|
|
421
|
+
this.#renderer.setAttribute(target, "aria-busy", "true");
|
|
422
|
+
// If a delayed teardown was pending, cancel it since new activity arrived
|
|
423
|
+
if (state.minDurationTimer != null) {
|
|
424
|
+
clearTimeout(state.minDurationTimer);
|
|
425
|
+
state.minDurationTimer = null;
|
|
426
|
+
}
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
#getWinningRequestForTarget(targetState) {
|
|
430
|
+
let winningRequest = null;
|
|
431
|
+
for (const requestId of targetState.activeRequestIds) {
|
|
432
|
+
const req = this.#requests.get(requestId);
|
|
433
|
+
if (req && req.status === "VISIBLE") {
|
|
434
|
+
if (!winningRequest || req.activatedAt >= winningRequest.activatedAt) {
|
|
435
|
+
winningRequest = req;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return winningRequest;
|
|
440
|
+
}
|
|
441
|
+
#normalizeTargets(target) {
|
|
442
|
+
if (!target) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
const rawList = Array.isArray(target) ? target : [target];
|
|
446
|
+
const result = [];
|
|
447
|
+
for (const item of rawList) {
|
|
448
|
+
if (item instanceof HTMLElement) {
|
|
449
|
+
result.push(item);
|
|
450
|
+
}
|
|
451
|
+
else if (item && typeof item === "object" && "nativeElement" in item && item.nativeElement instanceof HTMLElement) {
|
|
452
|
+
result.push(item.nativeElement);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
#teardownTarget(targetState) {
|
|
458
|
+
if (targetState.minDurationTimer != null) {
|
|
459
|
+
clearTimeout(targetState.minDurationTimer);
|
|
460
|
+
targetState.minDurationTimer = null;
|
|
461
|
+
}
|
|
462
|
+
targetState.cancelSubscription?.unsubscribe();
|
|
463
|
+
targetState.cancelSubscription = null;
|
|
464
|
+
if (targetState.componentRef != null) {
|
|
465
|
+
this.#appRef.detachView(targetState.componentRef.hostView);
|
|
466
|
+
targetState.componentRef.destroy();
|
|
467
|
+
targetState.componentRef = null;
|
|
468
|
+
}
|
|
469
|
+
targetState.visibleSince = undefined;
|
|
470
|
+
// Restore aria-busy attribute exactly
|
|
471
|
+
if (targetState.originalAriaBusy === null) {
|
|
472
|
+
this.#renderer.removeAttribute(targetState.target, "aria-busy");
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
this.#renderer.setAttribute(targetState.target, "aria-busy", targetState.originalAriaBusy);
|
|
476
|
+
}
|
|
477
|
+
// Restore target positioning safely
|
|
478
|
+
if (targetState.changedPosition) {
|
|
479
|
+
if (targetState.target.style.position === "relative") {
|
|
480
|
+
this.#renderer.setStyle(targetState.target, "position", targetState.originalInlinePosition || null);
|
|
481
|
+
}
|
|
482
|
+
targetState.changedPosition = false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
#updateRequest(requestId, update) {
|
|
486
|
+
const request = this.#requests.get(requestId);
|
|
487
|
+
if (!request || request.status === "CLOSED") {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (update.appearance !== undefined) {
|
|
491
|
+
request.appearance = update.appearance;
|
|
492
|
+
}
|
|
493
|
+
if (update.size !== undefined) {
|
|
494
|
+
request.size = update.size;
|
|
495
|
+
}
|
|
496
|
+
if (update.text !== undefined) {
|
|
497
|
+
request.text = update.text;
|
|
498
|
+
}
|
|
499
|
+
for (const target of request.targets) {
|
|
500
|
+
const targetState = this.#targets.get(target);
|
|
501
|
+
if (targetState) {
|
|
502
|
+
this.#updateTargetOverlay(targetState);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
#updateTargetOverlay(targetState) {
|
|
507
|
+
const winner = this.#getWinningRequestForTarget(targetState);
|
|
508
|
+
if (!winner) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (!targetState.componentRef) {
|
|
512
|
+
// Need containing block for absolute local positioning
|
|
513
|
+
if (!targetState.isFullPage) {
|
|
514
|
+
const defaultView = this.#document.defaultView;
|
|
515
|
+
const computedPosition = defaultView ? defaultView.getComputedStyle(targetState.target).position : "static";
|
|
516
|
+
if (computedPosition === "static") {
|
|
517
|
+
this.#renderer.setStyle(targetState.target, "position", "relative");
|
|
518
|
+
targetState.changedPosition = true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const compRef = createComponent(SpinnerOverlayComponent, {
|
|
522
|
+
environmentInjector: this.#appRef.injector,
|
|
523
|
+
elementInjector: this.#injector
|
|
524
|
+
});
|
|
525
|
+
compRef.setInput("appearance", winner.appearance);
|
|
526
|
+
compRef.setInput("size", winner.size);
|
|
527
|
+
compRef.setInput("text", winner.text);
|
|
528
|
+
compRef.setInput("cancellable", winner.cancellable);
|
|
529
|
+
compRef.setInput("cancelText", winner.cancelText);
|
|
530
|
+
compRef.setInput("fullPage", targetState.isFullPage);
|
|
531
|
+
if (winner.options.zIndex !== undefined) {
|
|
532
|
+
compRef.setInput("zIndex", winner.options.zIndex);
|
|
533
|
+
}
|
|
534
|
+
targetState.cancelSubscription = compRef.instance.cancel.subscribe(() => {
|
|
535
|
+
const currentWinner = this.#getWinningRequestForTarget(targetState);
|
|
536
|
+
if (currentWinner) {
|
|
537
|
+
this.#cancelRequest(currentWinner.id);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
this.#appRef.attachView(compRef.hostView);
|
|
541
|
+
this.#renderer.appendChild(targetState.target, compRef.location.nativeElement);
|
|
542
|
+
compRef.changeDetectorRef.detectChanges();
|
|
543
|
+
targetState.componentRef = compRef;
|
|
544
|
+
targetState.visibleSince = Date.now();
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// Physical overlay already exists: update its inputs dynamically
|
|
548
|
+
const compRef = targetState.componentRef;
|
|
549
|
+
compRef.setInput("appearance", winner.appearance);
|
|
550
|
+
compRef.setInput("size", winner.size);
|
|
551
|
+
compRef.setInput("text", winner.text);
|
|
552
|
+
compRef.setInput("cancellable", winner.cancellable);
|
|
553
|
+
compRef.setInput("cancelText", winner.cancelText);
|
|
554
|
+
compRef.setInput("fullPage", targetState.isFullPage);
|
|
555
|
+
if (winner.options.zIndex !== undefined) {
|
|
556
|
+
compRef.setInput("zIndex", winner.options.zIndex);
|
|
557
|
+
}
|
|
558
|
+
compRef.changeDetectorRef.detectChanges();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
562
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerService, providedIn: "root" }); }
|
|
563
|
+
}
|
|
564
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SpinnerService, decorators: [{
|
|
565
|
+
type: Injectable,
|
|
566
|
+
args: [{
|
|
567
|
+
providedIn: "root"
|
|
568
|
+
}]
|
|
569
|
+
}] });
|
|
570
|
+
|
|
571
|
+
/*
|
|
572
|
+
* Public API Surface of @nanahoshi/mona-ui/spinner
|
|
573
|
+
*/
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Generated bundle index. Do not edit.
|
|
577
|
+
*/
|
|
578
|
+
|
|
579
|
+
export { SpinnerComponent, SpinnerService };
|
|
580
|
+
//# sourceMappingURL=nanahoshi-mona-ui-spinner.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanahoshi/mona-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A very light Angular UI framework",
|
|
5
5
|
"author": "Nanahoshi",
|
|
6
6
|
"repository": {
|
|
@@ -289,6 +289,10 @@
|
|
|
289
289
|
"types": "./types/nanahoshi-mona-ui-slider.d.ts",
|
|
290
290
|
"default": "./fesm2022/nanahoshi-mona-ui-slider.mjs"
|
|
291
291
|
},
|
|
292
|
+
"./spinner": {
|
|
293
|
+
"types": "./types/nanahoshi-mona-ui-spinner.d.ts",
|
|
294
|
+
"default": "./fesm2022/nanahoshi-mona-ui-spinner.mjs"
|
|
295
|
+
},
|
|
292
296
|
"./split-button": {
|
|
293
297
|
"types": "./types/nanahoshi-mona-ui-split-button.d.ts",
|
|
294
298
|
"default": "./fesm2022/nanahoshi-mona-ui-split-button.mjs"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { ElementRef, OnDestroy } from '@angular/core';
|
|
3
|
+
import { LucideLoader } from '@lucide/angular';
|
|
4
|
+
import { ClassInputType } from '@nanahoshi/mona-ui/common';
|
|
5
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
6
|
+
import { VariantProps } from 'class-variance-authority';
|
|
7
|
+
import { VariantInputs } from '@nanahoshi/mona-ui/internal';
|
|
8
|
+
import { Observable } from 'rxjs';
|
|
9
|
+
|
|
10
|
+
type SpinnerAppearance = "default" | "pulsing" | "pulsing-triad" | "pulsing-ring" | "converging-spinner";
|
|
11
|
+
|
|
12
|
+
type SpinnerSize = "small" | "medium" | "large";
|
|
13
|
+
|
|
14
|
+
declare const spinnerThemeVariants: (props?: ({
|
|
15
|
+
size?: "small" | "medium" | "large" | null | undefined;
|
|
16
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
17
|
+
type SpinnerVariantProps = VariantProps<typeof spinnerThemeVariants>;
|
|
18
|
+
type SpinnerVariantInput = VariantInputs<SpinnerVariantProps>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @description
|
|
22
|
+
* An indeterminate loading indicator primitive for communicating ongoing activity.
|
|
23
|
+
* Supports multiple appearances, standard sizes, and accessible status announcements.
|
|
24
|
+
*/
|
|
25
|
+
declare class SpinnerComponent implements SpinnerVariantInput {
|
|
26
|
+
protected readonly baseClass: _angular_core.Signal<string>;
|
|
27
|
+
protected readonly loaderIcon: typeof LucideLoader;
|
|
28
|
+
protected readonly pixelSize: _angular_core.Signal<12 | 24 | 16>;
|
|
29
|
+
/**
|
|
30
|
+
* @description Visual appearance of the spinner animation.
|
|
31
|
+
* @default "default"
|
|
32
|
+
*/
|
|
33
|
+
readonly appearance: _angular_core.InputSignal<SpinnerAppearance>;
|
|
34
|
+
/**
|
|
35
|
+
* @description Accessible text announced by assistive technology when the spinner is not decorative.
|
|
36
|
+
* @default "Loading"
|
|
37
|
+
*/
|
|
38
|
+
readonly ariaLabel: _angular_core.InputSignal<string>;
|
|
39
|
+
/**
|
|
40
|
+
* @description When `true`, removes the `status` role and hides the spinner from assistive technology with `aria-hidden="true"`.
|
|
41
|
+
* Useful when a parent element already manages loading semantics (such as `aria-busy="true"` on a button).
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
readonly decorative: _angular_core.InputSignal<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* @description Size preset controlling the dimensions of the indicator.
|
|
47
|
+
* @default "medium"
|
|
48
|
+
*/
|
|
49
|
+
readonly size: _angular_core.InputSignal<SpinnerSize>;
|
|
50
|
+
/**
|
|
51
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
52
|
+
* @default ""
|
|
53
|
+
*/
|
|
54
|
+
readonly userClass: _angular_core.InputSignalWithTransform<string, ClassInputType>;
|
|
55
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SpinnerComponent, never>;
|
|
56
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SpinnerComponent, "mona-spinner", never, { "appearance": { "alias": "appearance"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "decorative": { "alias": "decorative"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SpinnerCancelOptions {
|
|
60
|
+
onCancel?: () => void;
|
|
61
|
+
text?: string;
|
|
62
|
+
}
|
|
63
|
+
interface SpinnerOptions {
|
|
64
|
+
appearance?: SpinnerAppearance;
|
|
65
|
+
cancellable?: boolean | SpinnerCancelOptions;
|
|
66
|
+
delay?: number;
|
|
67
|
+
id?: string;
|
|
68
|
+
minimumVisibleDuration?: number;
|
|
69
|
+
size?: SpinnerSize;
|
|
70
|
+
target?: HTMLElement | ElementRef<HTMLElement> | readonly (HTMLElement | ElementRef<HTMLElement>)[];
|
|
71
|
+
text?: string;
|
|
72
|
+
zIndex?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface SpinnerUpdate {
|
|
76
|
+
appearance?: SpinnerAppearance;
|
|
77
|
+
size?: SpinnerSize;
|
|
78
|
+
text?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SpinnerRef {
|
|
82
|
+
readonly cancelled$: Observable<void>;
|
|
83
|
+
readonly id: string;
|
|
84
|
+
close(): void;
|
|
85
|
+
update(update: SpinnerUpdate): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
declare class SpinnerService implements OnDestroy {
|
|
89
|
+
#private;
|
|
90
|
+
/**
|
|
91
|
+
* Hides and destroys the spinner associated with the given `SpinnerRef` or string `id`.
|
|
92
|
+
*/
|
|
93
|
+
hide(refOrId: SpinnerRef | string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Tears down every active/pending spinner request and physical overlay so
|
|
96
|
+
* nothing is left behind if the service outlives its consumers.
|
|
97
|
+
*/
|
|
98
|
+
ngOnDestroy(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Displays a spinner loading overlay according to the provided options.
|
|
101
|
+
* Returns a `SpinnerRef` handle for lifecycle management and updates.
|
|
102
|
+
*/
|
|
103
|
+
show(options?: SpinnerOptions): SpinnerRef;
|
|
104
|
+
/**
|
|
105
|
+
* Wraps an Observable operation with a Spinner lifecycle.
|
|
106
|
+
* Shows the spinner on subscription and closes it when the observable completes, errors, or is unsubscribed.
|
|
107
|
+
*/
|
|
108
|
+
track<T>(source: Observable<T>, options?: SpinnerOptions): Observable<T>;
|
|
109
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SpinnerService, never>;
|
|
110
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<SpinnerService>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { SpinnerComponent, SpinnerService };
|
|
114
|
+
export type { SpinnerAppearance, SpinnerCancelOptions, SpinnerOptions, SpinnerRef, SpinnerSize, SpinnerUpdate, SpinnerVariantProps };
|