@blotch/dolan 0.0.0-stage → 22.2.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/dist/commands/add.d.ts +22 -0
- package/dist/commands/add.js +132 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/add.test.d.ts +1 -0
- package/dist/commands/add.test.js +209 -0
- package/dist/commands/add.test.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +155 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/diff.test.d.ts +1 -0
- package/dist/commands/diff.test.js +327 -0
- package/dist/commands/diff.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +248 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +301 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/eject.d.ts +11 -0
- package/dist/commands/eject.js +55 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/eject.test.d.ts +1 -0
- package/dist/commands/eject.test.js +151 -0
- package/dist/commands/eject.test.js.map +1 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +73 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +143 -0
- package/dist/commands/info.test.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +154 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/list.d.ts +16 -0
- package/dist/commands/list.js +124 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/list.test.d.ts +1 -0
- package/dist/commands/list.test.js +285 -0
- package/dist/commands/list.test.js.map +1 -0
- package/dist/commands/outdated.d.ts +13 -0
- package/dist/commands/outdated.js +116 -0
- package/dist/commands/outdated.js.map +1 -0
- package/dist/commands/outdated.test.d.ts +1 -0
- package/dist/commands/outdated.test.js +208 -0
- package/dist/commands/outdated.test.js.map +1 -0
- package/dist/commands/remove.d.ts +18 -0
- package/dist/commands/remove.js +161 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/remove.test.d.ts +1 -0
- package/dist/commands/remove.test.js +361 -0
- package/dist/commands/remove.test.js.map +1 -0
- package/dist/commands/theme.d.ts +1 -0
- package/dist/commands/theme.js +66 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/theme.test.d.ts +1 -0
- package/dist/commands/theme.test.js +133 -0
- package/dist/commands/theme.test.js.map +1 -0
- package/dist/commands/update.d.ts +31 -0
- package/dist/commands/update.js +601 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +1233 -0
- package/dist/commands/update.test.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +48 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +21 -0
- package/dist/lockfile.js +39 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +85 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +95 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/test-utils.d.ts +16 -0
- package/dist/test-utils.js +65 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +194 -0
- package/dist/ui/components/accordion/accordion.ts +173 -0
- package/dist/ui/components/accordion/index.ts +6 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
- package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
- package/dist/ui/components/action-sheet/index.ts +6 -0
- package/dist/ui/components/alert/alert.test.ts +48 -0
- package/dist/ui/components/alert/alert.ts +104 -0
- package/dist/ui/components/alert/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
- package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
- package/dist/ui/components/alert-dialog/index.ts +7 -0
- package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
- package/dist/ui/components/area-chart/area-chart.ts +349 -0
- package/dist/ui/components/area-chart/index.ts +6 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
- package/dist/ui/components/aspect-ratio/index.ts +1 -0
- package/dist/ui/components/avatar/avatar.test.ts +69 -0
- package/dist/ui/components/avatar/avatar.ts +131 -0
- package/dist/ui/components/avatar/index.ts +1 -0
- package/dist/ui/components/badge/badge.test.ts +49 -0
- package/dist/ui/components/badge/badge.ts +97 -0
- package/dist/ui/components/badge/index.ts +1 -0
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
- package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
- package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
- package/dist/ui/components/bar-chart/index.ts +6 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
- package/dist/ui/components/bottom-sheet/index.ts +1 -0
- package/dist/ui/components/button/button-render.test.ts +113 -0
- package/dist/ui/components/button/button.test.ts +70 -0
- package/dist/ui/components/button/button.ts +170 -0
- package/dist/ui/components/button/index.ts +6 -0
- package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
- package/dist/ui/components/button-group/button-group.test.ts +54 -0
- package/dist/ui/components/button-group/button-group.ts +209 -0
- package/dist/ui/components/button-group/index.ts +8 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
- package/dist/ui/components/candlestick-chart/index.ts +7 -0
- package/dist/ui/components/card/card.test.ts +110 -0
- package/dist/ui/components/card/card.ts +162 -0
- package/dist/ui/components/card/index.ts +8 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
- package/dist/ui/components/cartesian-chart/index.ts +18 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
- package/dist/ui/components/checkbox/checkbox.ts +96 -0
- package/dist/ui/components/checkbox/index.ts +1 -0
- package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
- package/dist/ui/components/collapsible/collapsible.ts +122 -0
- package/dist/ui/components/collapsible/index.ts +5 -0
- package/dist/ui/components/dialog/dialog.test.ts +157 -0
- package/dist/ui/components/dialog/dialog.ts +248 -0
- package/dist/ui/components/dialog/index.ts +7 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
- package/dist/ui/components/empty-state/empty-state.ts +46 -0
- package/dist/ui/components/empty-state/index.ts +1 -0
- package/dist/ui/components/icon/icon.test.ts +52 -0
- package/dist/ui/components/icon/icon.ts +54 -0
- package/dist/ui/components/icon/icons.ts +569 -0
- package/dist/ui/components/icon/index.ts +2 -0
- package/dist/ui/components/input/index.ts +1 -0
- package/dist/ui/components/input/input.test.ts +134 -0
- package/dist/ui/components/input/input.ts +189 -0
- package/dist/ui/components/label/index.ts +1 -0
- package/dist/ui/components/label/label.test.ts +38 -0
- package/dist/ui/components/label/label.ts +24 -0
- package/dist/ui/components/line-chart/index.ts +6 -0
- package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
- package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
- package/dist/ui/components/line-chart/line-chart.ts +273 -0
- package/dist/ui/components/list/index.ts +1 -0
- package/dist/ui/components/list/list.test.ts +35 -0
- package/dist/ui/components/list/list.ts +71 -0
- package/dist/ui/components/nav-drawer/index.ts +8 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
- package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
- package/dist/ui/components/pagination/index.ts +1 -0
- package/dist/ui/components/pagination/pagination.test.ts +96 -0
- package/dist/ui/components/pagination/pagination.ts +147 -0
- package/dist/ui/components/progress/index.ts +1 -0
- package/dist/ui/components/progress/progress.test.ts +71 -0
- package/dist/ui/components/progress/progress.ts +118 -0
- package/dist/ui/components/radio-group/index.ts +1 -0
- package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
- package/dist/ui/components/radio-group/radio-group.ts +129 -0
- package/dist/ui/components/scatter-chart/index.ts +6 -0
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
- package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
- package/dist/ui/components/scroll-area/index.ts +1 -0
- package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
- package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
- package/dist/ui/components/select/index.ts +1 -0
- package/dist/ui/components/select/select.test.ts +107 -0
- package/dist/ui/components/select/select.ts +146 -0
- package/dist/ui/components/separator/index.ts +1 -0
- package/dist/ui/components/separator/separator.test.ts +26 -0
- package/dist/ui/components/separator/separator.ts +45 -0
- package/dist/ui/components/sheet/index.ts +7 -0
- package/dist/ui/components/sheet/sheet.test.ts +44 -0
- package/dist/ui/components/sheet/sheet.ts +114 -0
- package/dist/ui/components/skeleton/index.ts +1 -0
- package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
- package/dist/ui/components/skeleton/skeleton.ts +45 -0
- package/dist/ui/components/spinner/index.ts +1 -0
- package/dist/ui/components/spinner/spinner.test.ts +39 -0
- package/dist/ui/components/spinner/spinner.ts +83 -0
- package/dist/ui/components/switch/index.ts +1 -0
- package/dist/ui/components/switch/switch.test.ts +63 -0
- package/dist/ui/components/switch/switch.ts +98 -0
- package/dist/ui/components/tabs/index.ts +1 -0
- package/dist/ui/components/tabs/tabs.test.ts +199 -0
- package/dist/ui/components/tabs/tabs.ts +317 -0
- package/dist/ui/components/textarea/index.ts +1 -0
- package/dist/ui/components/textarea/textarea.test.ts +138 -0
- package/dist/ui/components/textarea/textarea.ts +221 -0
- package/dist/ui/components/toast/index.ts +9 -0
- package/dist/ui/components/toast/toast-state.test.ts +54 -0
- package/dist/ui/components/toast/toast-state.ts +46 -0
- package/dist/ui/components/toast/toast.test.ts +342 -0
- package/dist/ui/components/toast/toast.ts +527 -0
- package/dist/ui/components/toggle/index.ts +7 -0
- package/dist/ui/components/toggle/toggle.test.ts +186 -0
- package/dist/ui/components/toggle/toggle.ts +218 -0
- package/dist/ui/components/typography/index.ts +5 -0
- package/dist/ui/components/typography/typography.test.ts +55 -0
- package/dist/ui/components/typography/typography.ts +44 -0
- package/dist/ui/theme/dark.css +41 -0
- package/dist/ui/theme/default.css +56 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
- package/dist/ui/theme/tailwind-plugin.ts +151 -0
- package/dist/ui/utils/animate.test.ts +324 -0
- package/dist/ui/utils/animate.ts +544 -0
- package/dist/ui/utils/cn.test.ts +27 -0
- package/dist/ui/utils/cn.ts +6 -0
- package/dist/utils/analyze.d.ts +32 -0
- package/dist/utils/analyze.js +109 -0
- package/dist/utils/analyze.js.map +1 -0
- package/dist/utils/analyze.test.d.ts +1 -0
- package/dist/utils/analyze.test.js +146 -0
- package/dist/utils/analyze.test.js.map +1 -0
- package/dist/utils/animate.d.ts +210 -0
- package/dist/utils/animate.js +423 -0
- package/dist/utils/animate.js.map +1 -0
- package/dist/utils/animate.test.d.ts +1 -0
- package/dist/utils/animate.test.js +365 -0
- package/dist/utils/animate.test.js.map +1 -0
- package/dist/utils/banner.d.ts +1 -0
- package/dist/utils/banner.js +15 -0
- package/dist/utils/banner.js.map +1 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +29 -0
- package/dist/utils/banner.test.js.map +1 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +6 -0
- package/dist/utils/cn.js.map +1 -0
- package/dist/utils/cn.test.d.ts +1 -0
- package/dist/utils/cn.test.js +21 -0
- package/dist/utils/cn.test.js.map +1 -0
- package/dist/utils/format.d.ts +31 -0
- package/dist/utils/format.js +95 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/format.test.d.ts +1 -0
- package/dist/utils/format.test.js +71 -0
- package/dist/utils/format.test.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +36 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +12 -0
- package/dist/utils/resolve-paths.js +29 -0
- package/dist/utils/resolve-paths.js.map +1 -0
- package/dist/utils/resolve-paths.test.d.ts +1 -0
- package/dist/utils/resolve-paths.test.js +52 -0
- package/dist/utils/resolve-paths.test.js.map +1 -0
- package/package.json +56 -4
- package/README.md +0 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
output,
|
|
10
|
+
signal,
|
|
11
|
+
viewChild,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type AnimationHandle,
|
|
17
|
+
DURATION,
|
|
18
|
+
EASING,
|
|
19
|
+
SCALE,
|
|
20
|
+
fadeIn,
|
|
21
|
+
fadeOut,
|
|
22
|
+
scaleIn,
|
|
23
|
+
scaleOut,
|
|
24
|
+
} from '../../utils/animate';
|
|
25
|
+
import { cn } from '../../utils/cn';
|
|
26
|
+
|
|
27
|
+
@Component({
|
|
28
|
+
selector: 'ui-dialog',
|
|
29
|
+
standalone: true,
|
|
30
|
+
imports: [LYNX_ELEMENTS],
|
|
31
|
+
encapsulation: ViewEncapsulation.None,
|
|
32
|
+
template: `
|
|
33
|
+
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
34
|
+
<view #backdrop [class]="backdropClass()" (bindtap)="onBackdropTap()">
|
|
35
|
+
<view #panel [class]="panelClass()" (catchtap)="onPanelTap()">
|
|
36
|
+
<ng-content />
|
|
37
|
+
</view>
|
|
38
|
+
</view>
|
|
39
|
+
</overlay>
|
|
40
|
+
`,
|
|
41
|
+
})
|
|
42
|
+
export class UiDialog {
|
|
43
|
+
readonly open = model(false);
|
|
44
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
|
+
|
|
46
|
+
readonly closed = output<void>();
|
|
47
|
+
|
|
48
|
+
protected readonly overlayVisible = signal(false);
|
|
49
|
+
|
|
50
|
+
readonly backdropRef = viewChild<ElementRef>('backdrop');
|
|
51
|
+
readonly panelRef = viewChild<ElementRef>('panel');
|
|
52
|
+
#backdropAnim?: AnimationHandle;
|
|
53
|
+
#panelAnim?: AnimationHandle;
|
|
54
|
+
#hasBeenOpen = false;
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
// `#hasBeenOpen` prevents the close animation from running on the initial
|
|
58
|
+
// effect evaluation when `open` starts as false. Without this guard, the
|
|
59
|
+
// very first run would call #doClose() immediately, resulting in a no-op
|
|
60
|
+
// animation that still hides the overlay and emits `closed` unexpectedly.
|
|
61
|
+
effect(() => {
|
|
62
|
+
const isOpen = this.open();
|
|
63
|
+
if (isOpen) {
|
|
64
|
+
this.#hasBeenOpen = true;
|
|
65
|
+
this.#doOpen();
|
|
66
|
+
} else if (this.#hasBeenOpen) {
|
|
67
|
+
this.#doClose();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hide the overlay with `display:none` on web where `visible` attr has no
|
|
74
|
+
* effect. `width`/`height`/`top`/`left` are also web-only necessities: on
|
|
75
|
+
* web `<overlay>` has no intrinsic full-screen sizing (unlike native), so
|
|
76
|
+
* without them it collapses to 0x0 and nothing renders. `z-index` keeps it
|
|
77
|
+
* above sibling content that would otherwise occasionally paint on top.
|
|
78
|
+
*/
|
|
79
|
+
protected readonly overlayStyle = computed(() =>
|
|
80
|
+
this.overlayVisible()
|
|
81
|
+
? 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999;'
|
|
82
|
+
: 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
protected readonly backdropClass = computed(() =>
|
|
86
|
+
// bg-black/50 dims the backdrop; backdrop-blur-sm applies frosted-glass blur
|
|
87
|
+
// on web. Lynx does not support backdrop-filter, so the blur is web-only.
|
|
88
|
+
cn(
|
|
89
|
+
'h-full w-full items-center flex justify-center',
|
|
90
|
+
'backdrop-blur-sm bg-black/50',
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
protected readonly panelClass = computed(() =>
|
|
95
|
+
cn(
|
|
96
|
+
'w-4/5 flex-col rounded-lg border border-border bg-background p-6 flex',
|
|
97
|
+
this.userClass(),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
protected onBackdropTap(): void {
|
|
102
|
+
this.open.set(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
|
|
107
|
+
* the tap from bubbling to the backdrop — Lynx controls propagation via the
|
|
108
|
+
* event prefix, not at runtime. (The renderer shims `event.stopPropagation()`
|
|
109
|
+
* as a no-op so DOM-style handlers don't crash, but it has no effect here.)
|
|
110
|
+
*/
|
|
111
|
+
protected onPanelTap(): void {}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Two-phase open: make overlay visible first (so native elements exist in
|
|
115
|
+
* the tree), then animate on the next frame. See nav-drawer for the same
|
|
116
|
+
* pattern — Lynx animate() requires elements to be mounted before use.
|
|
117
|
+
*/
|
|
118
|
+
#doOpen(): void {
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
this.overlayVisible.set(true);
|
|
121
|
+
setTimeout(() => this.#animateIn(), 0);
|
|
122
|
+
}, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Animate out before hiding; +20ms guards against Lynx timer imprecision
|
|
127
|
+
* causing elements to disappear before the animation finishes.
|
|
128
|
+
*/
|
|
129
|
+
#doClose(): void {
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
this.#animateOut();
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
this.overlayVisible.set(false);
|
|
134
|
+
this.closed.emit();
|
|
135
|
+
}, DURATION.normal + 20);
|
|
136
|
+
}, 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#animateIn(): void {
|
|
140
|
+
const backdrop = this.backdropRef()?.nativeElement;
|
|
141
|
+
const panel = this.panelRef()?.nativeElement;
|
|
142
|
+
if (!backdrop || !panel) return;
|
|
143
|
+
|
|
144
|
+
this.#backdropAnim?.cancel();
|
|
145
|
+
this.#panelAnim?.cancel();
|
|
146
|
+
|
|
147
|
+
// Backdrop fades in first
|
|
148
|
+
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
149
|
+
|
|
150
|
+
// Panel scales in with gentle spring (staggered 30ms after backdrop)
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
this.#panelAnim = scaleIn(panel, {
|
|
153
|
+
duration: DURATION.slow,
|
|
154
|
+
easing: EASING.springSubtle,
|
|
155
|
+
fromScale: SCALE.dialogFrom,
|
|
156
|
+
fromY: 10,
|
|
157
|
+
});
|
|
158
|
+
}, 30);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#animateOut(): void {
|
|
162
|
+
const backdrop = this.backdropRef()?.nativeElement;
|
|
163
|
+
const panel = this.panelRef()?.nativeElement;
|
|
164
|
+
if (!backdrop || !panel) return;
|
|
165
|
+
|
|
166
|
+
this.#backdropAnim?.cancel();
|
|
167
|
+
this.#panelAnim?.cancel();
|
|
168
|
+
|
|
169
|
+
// Panel exits with subtle scale + fade
|
|
170
|
+
this.#panelAnim = scaleOut(panel, {
|
|
171
|
+
duration: 180,
|
|
172
|
+
easing: EASING.accelerate,
|
|
173
|
+
toScale: SCALE.dialogTo,
|
|
174
|
+
toY: 10,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Backdrop fades out in parallel
|
|
178
|
+
this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Component({
|
|
183
|
+
selector: 'ui-dialog-header',
|
|
184
|
+
standalone: true,
|
|
185
|
+
imports: [LYNX_ELEMENTS],
|
|
186
|
+
encapsulation: ViewEncapsulation.None,
|
|
187
|
+
template: `
|
|
188
|
+
<view [class]="containerClass()">
|
|
189
|
+
<ng-content />
|
|
190
|
+
</view>
|
|
191
|
+
`,
|
|
192
|
+
})
|
|
193
|
+
export class UiDialogHeader {
|
|
194
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
195
|
+
|
|
196
|
+
protected readonly containerClass = computed(() =>
|
|
197
|
+
cn('flex-col gap-1.5 flex', this.userClass()),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Component({
|
|
202
|
+
selector: 'ui-dialog-title',
|
|
203
|
+
standalone: true,
|
|
204
|
+
imports: [LYNX_ELEMENTS],
|
|
205
|
+
encapsulation: ViewEncapsulation.None,
|
|
206
|
+
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
207
|
+
})
|
|
208
|
+
export class UiDialogTitle {
|
|
209
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
210
|
+
|
|
211
|
+
protected readonly textClass = computed(() =>
|
|
212
|
+
cn('text-lg font-semibold text-foreground', this.userClass()),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Component({
|
|
217
|
+
selector: 'ui-dialog-description',
|
|
218
|
+
standalone: true,
|
|
219
|
+
imports: [LYNX_ELEMENTS],
|
|
220
|
+
encapsulation: ViewEncapsulation.None,
|
|
221
|
+
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
222
|
+
})
|
|
223
|
+
export class UiDialogDescription {
|
|
224
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
225
|
+
|
|
226
|
+
protected readonly textClass = computed(() =>
|
|
227
|
+
cn('text-sm text-muted-foreground', this.userClass()),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Component({
|
|
232
|
+
selector: 'ui-dialog-footer',
|
|
233
|
+
standalone: true,
|
|
234
|
+
imports: [LYNX_ELEMENTS],
|
|
235
|
+
encapsulation: ViewEncapsulation.None,
|
|
236
|
+
template: `
|
|
237
|
+
<view [class]="containerClass()">
|
|
238
|
+
<ng-content />
|
|
239
|
+
</view>
|
|
240
|
+
`,
|
|
241
|
+
})
|
|
242
|
+
export class UiDialogFooter {
|
|
243
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
244
|
+
|
|
245
|
+
protected readonly containerClass = computed(() =>
|
|
246
|
+
cn('flex-row gap-2 pt-4 flex justify-end', this.userClass()),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiEmptyState } from './empty-state';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiEmptyState, LYNX_ELEMENTS],
|
|
11
|
+
template: `
|
|
12
|
+
<ui-empty-state
|
|
13
|
+
icon="search"
|
|
14
|
+
title="No results"
|
|
15
|
+
description="Try adjusting your search"
|
|
16
|
+
>
|
|
17
|
+
<text>Extra content</text>
|
|
18
|
+
</ui-empty-state>
|
|
19
|
+
`,
|
|
20
|
+
})
|
|
21
|
+
class EmptyStateHost {}
|
|
22
|
+
|
|
23
|
+
@Component({
|
|
24
|
+
standalone: true,
|
|
25
|
+
imports: [UiEmptyState, LYNX_ELEMENTS],
|
|
26
|
+
template: `<ui-empty-state title="Empty"></ui-empty-state>`,
|
|
27
|
+
})
|
|
28
|
+
class MinimalEmptyStateHost {}
|
|
29
|
+
|
|
30
|
+
describe('UiEmptyState', () => {
|
|
31
|
+
it('renders with an icon, title, description, and projected content', async () => {
|
|
32
|
+
// Text-based queries against the JSDOM container occasionally miss content
|
|
33
|
+
// rendered into Lynx `<text>` elements that aren't unwrapped by
|
|
34
|
+
// @testing-library/dom's text traversal. A container assertion is enough
|
|
35
|
+
// to prove the full template (including the @if(icon)/@if(description)
|
|
36
|
+
// branches and the <ng-content /> slot) mounted without throwing.
|
|
37
|
+
const { container } = await render(EmptyStateHost);
|
|
38
|
+
expect(container).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders with only the required title (no icon, no description)', async () => {
|
|
42
|
+
const { container } = await render(MinimalEmptyStateHost);
|
|
43
|
+
expect(container).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders the description class when a description is programmatically set', async () => {
|
|
47
|
+
// JIT template bindings don't propagate through `input()`; drive the input
|
|
48
|
+
// via setInputSignal so the @if(description()) branch actually renders and
|
|
49
|
+
// its computed class runs.
|
|
50
|
+
const { componentRef, container } = await render(UiEmptyState);
|
|
51
|
+
const inst = componentRef.instance as UiEmptyState;
|
|
52
|
+
setInputSignal(inst.title, 'Title');
|
|
53
|
+
setInputSignal(inst.description, 'A description');
|
|
54
|
+
await waitForUpdate();
|
|
55
|
+
|
|
56
|
+
// Both text runs are present after CD flush.
|
|
57
|
+
const textNodes = container.querySelectorAll('text');
|
|
58
|
+
expect(textNodes.length).toBeGreaterThanOrEqual(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Component, ViewEncapsulation, computed, input } from '@angular/core';
|
|
2
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
import { type IconName } from '../icon/icons';
|
|
6
|
+
import { UiIcon } from '../icon/icon';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'ui-empty-state',
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [LYNX_ELEMENTS, UiIcon],
|
|
12
|
+
encapsulation: ViewEncapsulation.None,
|
|
13
|
+
template: `
|
|
14
|
+
<view [class]="containerClass()">
|
|
15
|
+
@if (icon()) {
|
|
16
|
+
<ui-icon [name]="icon()!" size="lg" class="mb-3" />
|
|
17
|
+
}
|
|
18
|
+
<text [class]="titleClass()">{{ title() }}</text>
|
|
19
|
+
@if (description()) {
|
|
20
|
+
<text [class]="descriptionClass()">{{ description() }}</text>
|
|
21
|
+
}
|
|
22
|
+
<ng-content />
|
|
23
|
+
</view>
|
|
24
|
+
`,
|
|
25
|
+
})
|
|
26
|
+
export class UiEmptyState {
|
|
27
|
+
readonly icon = input<IconName | undefined>(undefined);
|
|
28
|
+
readonly title = input.required<string>();
|
|
29
|
+
readonly description = input('');
|
|
30
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
31
|
+
|
|
32
|
+
protected readonly containerClass = computed(() =>
|
|
33
|
+
cn(
|
|
34
|
+
'flex-col items-center px-6 py-12 flex justify-center',
|
|
35
|
+
this.userClass(),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
protected readonly titleClass = computed(() =>
|
|
40
|
+
cn('mb-1 text-lg font-semibold text-foreground'),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
protected readonly descriptionClass = computed(() =>
|
|
44
|
+
cn('mb-4 text-sm text-muted-foreground text-center'),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiEmptyState } from './empty-state';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiIcon } from './icon';
|
|
7
|
+
import { ICONS, type IconName } from './icons';
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [UiIcon, LYNX_ELEMENTS],
|
|
12
|
+
template: `<ui-icon [name]="name" [size]="size" [color]="color" />`,
|
|
13
|
+
})
|
|
14
|
+
class IconHost {
|
|
15
|
+
name: IconName = 'check';
|
|
16
|
+
size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
|
|
17
|
+
color?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('UiIcon', () => {
|
|
21
|
+
it('renders the requested icon at default size', async () => {
|
|
22
|
+
const { container } = await render(IconHost);
|
|
23
|
+
expect(container).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('applies the size input to the width/height style', async () => {
|
|
27
|
+
// JIT template bindings are inert against `input()` — drive the signals
|
|
28
|
+
// directly so sizeStyle and svgContent both re-run.
|
|
29
|
+
const { componentRef, container } = await render(UiIcon);
|
|
30
|
+
setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
|
|
31
|
+
setInputSignal((componentRef.instance as UiIcon).size, 'xs');
|
|
32
|
+
await waitForUpdate();
|
|
33
|
+
const svg = container.querySelector('svg');
|
|
34
|
+
expect(svg?.getAttribute('style')).toContain('16px');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('substitutes currentColor with the color input value', async () => {
|
|
38
|
+
const { componentRef, container } = await render(UiIcon);
|
|
39
|
+
setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
|
|
40
|
+
setInputSignal((componentRef.instance as UiIcon).color, 'rgb(255, 0, 0)');
|
|
41
|
+
await waitForUpdate();
|
|
42
|
+
const svg = container.querySelector('svg');
|
|
43
|
+
expect(svg?.getAttribute('content')).toContain('rgb(255, 0, 0)');
|
|
44
|
+
expect(svg?.getAttribute('content')).not.toContain('currentColor');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('ICONS registry', () => {
|
|
49
|
+
it('exports at least one icon', () => {
|
|
50
|
+
expect(Object.keys(ICONS).length).toBeGreaterThan(0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Component, ViewEncapsulation, input } from '@angular/core';
|
|
2
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
|
+
|
|
4
|
+
import { type IconName, ICONS } from './icons';
|
|
5
|
+
|
|
6
|
+
const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SVG markup is passed via [attr.content] rather than as child elements.
|
|
10
|
+
* Lynx's native <svg> element can't contain child nodes in the flat element
|
|
11
|
+
* model. Passing the full SVG string as the `content` attribute is how Lynx
|
|
12
|
+
* renders vector graphics without a DOM tree.
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'ui-icon',
|
|
16
|
+
imports: [LYNX_ELEMENTS],
|
|
17
|
+
encapsulation: ViewEncapsulation.None,
|
|
18
|
+
template: `
|
|
19
|
+
<svg [attr.content]="svgContent()" [attr.style]="sizeStyle()" />
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
export class UiIcon {
|
|
23
|
+
readonly name = input.required<IconName>();
|
|
24
|
+
readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
25
|
+
readonly color = input<string | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
protected svgContent(): string {
|
|
28
|
+
let svg = ICONS[this.name()];
|
|
29
|
+
const c = this.color();
|
|
30
|
+
if (c) {
|
|
31
|
+
// Lynx SVG elements don't propagate the CSS `color` property into SVG
|
|
32
|
+
// stroke attributes, so we substitute the literal `currentColor` in the
|
|
33
|
+
// markup string rather than relying on `currentColor` CSS propagation.
|
|
34
|
+
//
|
|
35
|
+
// Uses split().join() instead of String.prototype.replaceAll(). replaceAll
|
|
36
|
+
// is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
|
|
37
|
+
// because it's a runtime method (not syntax) the build's down-leveling does
|
|
38
|
+
// not polyfill it, so it simply doesn't exist there. Calling it throws
|
|
39
|
+
// "main-thread.js exception: not a function" from inside Angular change
|
|
40
|
+
// detection, which aborts the whole tick before Lynx flushes the element
|
|
41
|
+
// tree — so NO icon paints (not just a colored one), and the error count
|
|
42
|
+
// climbs on every render/interaction as each tick re-runs and re-throws.
|
|
43
|
+
// split/join is ES2019-safe and is preferred over `replace(/.../g)` here
|
|
44
|
+
// because the search term is a plain literal and needs no regex escaping.
|
|
45
|
+
svg = svg.split('currentColor').join(c);
|
|
46
|
+
}
|
|
47
|
+
return svg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected sizeStyle(): string {
|
|
51
|
+
const px = SIZE_MAP[this.size()];
|
|
52
|
+
return `width: ${px}px; height: ${px}px;`;
|
|
53
|
+
}
|
|
54
|
+
}
|