@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,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
model,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
9
|
+
|
|
10
|
+
import { cn } from '../../utils/cn';
|
|
11
|
+
|
|
12
|
+
const CHEVRON_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`;
|
|
13
|
+
const CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`;
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ui-pagination',
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [LYNX_ELEMENTS],
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
template: `
|
|
21
|
+
<view [class]="containerClass()">
|
|
22
|
+
<view [class]="navButtonClass(page() <= 1)" (bindtap)="prev()">
|
|
23
|
+
<svg
|
|
24
|
+
[attr.content]="chevronLeftSvg"
|
|
25
|
+
style="width: 16px; height: 16px;"
|
|
26
|
+
/>
|
|
27
|
+
</view>
|
|
28
|
+
|
|
29
|
+
@for (item of visiblePages(); track item.key) {
|
|
30
|
+
@if (item.type === 'page') {
|
|
31
|
+
<view
|
|
32
|
+
[class]="pageButtonClass(item.value === page())"
|
|
33
|
+
(bindtap)="goTo(item.value)"
|
|
34
|
+
>
|
|
35
|
+
<text [class]="pageTextClass(item.value === page())">{{
|
|
36
|
+
item.value
|
|
37
|
+
}}</text>
|
|
38
|
+
</view>
|
|
39
|
+
} @else {
|
|
40
|
+
<view class="h-8 w-8 items-center flex justify-center">
|
|
41
|
+
<text class="text-xs text-muted-foreground">...</text>
|
|
42
|
+
</view>
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
<view [class]="navButtonClass(page() >= totalPages())" (bindtap)="next()">
|
|
47
|
+
<svg
|
|
48
|
+
[attr.content]="chevronRightSvg"
|
|
49
|
+
style="width: 16px; height: 16px;"
|
|
50
|
+
/>
|
|
51
|
+
</view>
|
|
52
|
+
</view>
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
export class UiPagination {
|
|
56
|
+
readonly page = model(1);
|
|
57
|
+
readonly totalPages = input.required<number>();
|
|
58
|
+
readonly siblingCount = input(1);
|
|
59
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
60
|
+
|
|
61
|
+
protected readonly chevronLeftSvg = CHEVRON_LEFT;
|
|
62
|
+
protected readonly chevronRightSvg = CHEVRON_RIGHT;
|
|
63
|
+
|
|
64
|
+
// Sliding-window pagination algorithm:
|
|
65
|
+
// Always shows page 1 and the last page. Around the current page, shows
|
|
66
|
+
// `siblingCount` neighbors on each side. If there's a gap between page 1
|
|
67
|
+
// and the window (or window and last page), an ellipsis is inserted.
|
|
68
|
+
// The threshold `5 + siblings * 2` is the max pages that fit without
|
|
69
|
+
// truncation: first + last + up to (2*siblings+1) window + 2 ellipsis slots.
|
|
70
|
+
protected readonly visiblePages = computed(() => {
|
|
71
|
+
const total = this.totalPages();
|
|
72
|
+
const current = this.page();
|
|
73
|
+
const siblings = this.siblingCount();
|
|
74
|
+
|
|
75
|
+
if (total <= 5 + siblings * 2) {
|
|
76
|
+
return Array.from({ length: total }, (_, i) => ({
|
|
77
|
+
type: 'page' as const,
|
|
78
|
+
value: i + 1,
|
|
79
|
+
key: `p${i + 1}`,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const items: { type: 'page' | 'ellipsis'; value: number; key: string }[] =
|
|
84
|
+
[];
|
|
85
|
+
|
|
86
|
+
items.push({ type: 'page', value: 1, key: 'p1' });
|
|
87
|
+
|
|
88
|
+
const rangeStart = Math.max(2, current - siblings);
|
|
89
|
+
const rangeEnd = Math.min(total - 1, current + siblings);
|
|
90
|
+
|
|
91
|
+
if (rangeStart > 2) {
|
|
92
|
+
items.push({ type: 'ellipsis', value: 0, key: 'el' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
96
|
+
items.push({ type: 'page', value: i, key: `p${i}` });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (rangeEnd < total - 1) {
|
|
100
|
+
items.push({ type: 'ellipsis', value: 0, key: 'er' });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
items.push({ type: 'page', value: total, key: `p${total}` });
|
|
104
|
+
return items;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
protected readonly containerClass = computed(() =>
|
|
108
|
+
cn('flex-row items-center gap-1 flex', this.userClass()),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
protected navButtonClass(disabled: boolean): string {
|
|
112
|
+
return cn(
|
|
113
|
+
'h-10 w-10 items-center rounded-md flex justify-center',
|
|
114
|
+
disabled && 'opacity-50',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected pageButtonClass(active: boolean): string {
|
|
119
|
+
return cn(
|
|
120
|
+
'h-10 w-10 items-center rounded-md flex justify-center',
|
|
121
|
+
active ? 'bg-primary' : 'bg-transparent',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected pageTextClass(active: boolean): string {
|
|
126
|
+
return cn(
|
|
127
|
+
'text-sm font-medium',
|
|
128
|
+
active ? 'text-primary-foreground' : 'text-foreground',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected prev(): void {
|
|
133
|
+
if (this.page() > 1) {
|
|
134
|
+
this.page.update((p) => p - 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected next(): void {
|
|
139
|
+
if (this.page() < this.totalPages()) {
|
|
140
|
+
this.page.update((p) => p + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
protected goTo(p: number): void {
|
|
145
|
+
this.page.set(p);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiProgress } from './progress';
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { UiProgress } from './progress';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiProgress, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-progress [value]="value" [indeterminate]="indeterminate" />`,
|
|
12
|
+
})
|
|
13
|
+
class ProgressHost {
|
|
14
|
+
value = 40;
|
|
15
|
+
indeterminate = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('UiProgress', () => {
|
|
19
|
+
it('renders at the given value', async () => {
|
|
20
|
+
const { container } = await render(ProgressHost);
|
|
21
|
+
expect(container).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders indeterminate mode without throwing', async () => {
|
|
25
|
+
@Component({
|
|
26
|
+
standalone: true,
|
|
27
|
+
imports: [UiProgress, LYNX_ELEMENTS],
|
|
28
|
+
template: `<ui-progress [indeterminate]="true" />`,
|
|
29
|
+
})
|
|
30
|
+
class IndeterminateHost {}
|
|
31
|
+
const { container } = await render(IndeterminateHost);
|
|
32
|
+
expect(container).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('scales the fill width by the value/max ratio', async () => {
|
|
36
|
+
// JIT template bindings are inert against input(); drive the signals so
|
|
37
|
+
// the percent/fillStyle computed signals actually run.
|
|
38
|
+
const { componentRef, container } = await render(UiProgress);
|
|
39
|
+
setInputSignal((componentRef.instance as UiProgress).value, 25);
|
|
40
|
+
setInputSignal((componentRef.instance as UiProgress).max, 100);
|
|
41
|
+
await waitForUpdate();
|
|
42
|
+
const fill = container.querySelectorAll('view')[1];
|
|
43
|
+
expect(fill?.getAttribute('style')).toContain('width: 25%');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clamps the fill to 100% when the value exceeds max', async () => {
|
|
47
|
+
const { componentRef, container } = await render(UiProgress);
|
|
48
|
+
setInputSignal((componentRef.instance as UiProgress).value, 500);
|
|
49
|
+
setInputSignal((componentRef.instance as UiProgress).max, 100);
|
|
50
|
+
await waitForUpdate();
|
|
51
|
+
const fill = container.querySelectorAll('view')[1];
|
|
52
|
+
expect(fill?.getAttribute('style')).toContain('width: 100%');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('clamps the fill to 0% when max is zero (division safety)', async () => {
|
|
56
|
+
const { componentRef, container } = await render(UiProgress);
|
|
57
|
+
setInputSignal((componentRef.instance as UiProgress).value, 50);
|
|
58
|
+
setInputSignal((componentRef.instance as UiProgress).max, 0);
|
|
59
|
+
await waitForUpdate();
|
|
60
|
+
const fill = container.querySelectorAll('view')[1];
|
|
61
|
+
expect(fill?.getAttribute('style')).toContain('width: 0%');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses the fixed 40% fill width when indeterminate is true', async () => {
|
|
65
|
+
const { componentRef, container } = await render(UiProgress);
|
|
66
|
+
setInputSignal((componentRef.instance as UiProgress).indeterminate, true);
|
|
67
|
+
await waitForUpdate();
|
|
68
|
+
const fill = container.querySelectorAll('view')[1];
|
|
69
|
+
expect(fill?.getAttribute('style')).toContain('width: 40%');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
input,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
|
+
|
|
12
|
+
import { type AnimationHandle, DURATION, EASING } from '../../utils/animate';
|
|
13
|
+
import { cn } from '../../utils/cn';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ui-progress',
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [LYNX_ELEMENTS],
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
template: `
|
|
21
|
+
<view [class]="trackClass()">
|
|
22
|
+
<view #fill [class]="fillClass()" [style]="fillStyle()" />
|
|
23
|
+
</view>
|
|
24
|
+
`,
|
|
25
|
+
})
|
|
26
|
+
export class UiProgress {
|
|
27
|
+
readonly value = input(0);
|
|
28
|
+
readonly max = input(100);
|
|
29
|
+
/**
|
|
30
|
+
* When true, shows an indeterminate (looping) animation
|
|
31
|
+
*/
|
|
32
|
+
readonly indeterminate = input(false);
|
|
33
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
34
|
+
|
|
35
|
+
readonly fillRef = viewChild<ElementRef>('fill');
|
|
36
|
+
#fillAnim?: AnimationHandle;
|
|
37
|
+
#indeterminateAnim?: AnimationHandle;
|
|
38
|
+
#previousPercent: number | null = null;
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
effect(() => {
|
|
42
|
+
const el = this.fillRef()?.nativeElement;
|
|
43
|
+
// el.animate() is inert in jsdom. Determinate and indeterminate paths
|
|
44
|
+
// are exercised on-device; the effect's animation branch cannot be
|
|
45
|
+
// meaningfully verified from a unit test.
|
|
46
|
+
/* v8 ignore start */
|
|
47
|
+
if (!el) return;
|
|
48
|
+
|
|
49
|
+
// Handle indeterminate mode with infinite sliding animation.
|
|
50
|
+
// The fill element is 40% of the track width. `translateX` percentages
|
|
51
|
+
// are relative to the element's OWN width, so:
|
|
52
|
+
// translateX(-100%) = left edge at -40% of track = starts off-screen left
|
|
53
|
+
// translateX(250%) = left edge at +100% of track = exits off-screen right
|
|
54
|
+
// 250% is the minimum that pushes the 40%-wide fill fully past the right edge.
|
|
55
|
+
if (this.indeterminate()) {
|
|
56
|
+
this.#fillAnim?.cancel();
|
|
57
|
+
this.#indeterminateAnim?.cancel();
|
|
58
|
+
this.#indeterminateAnim = el.animate(
|
|
59
|
+
[
|
|
60
|
+
{ transform: 'translateX(-100%)', width: '40%' },
|
|
61
|
+
{ transform: 'translateX(250%)', width: '40%' },
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
duration: 1200,
|
|
65
|
+
easing: EASING.standard,
|
|
66
|
+
iterations: Infinity,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determinate mode — animate width changes smoothly.
|
|
73
|
+
// `#previousPercent === null` skips the very first effect run so
|
|
74
|
+
// the bar doesn't animate from 0% → initial value on mount.
|
|
75
|
+
// The equality guard prevents redundant animations if the effect
|
|
76
|
+
// re-runs without an actual percent change.
|
|
77
|
+
this.#indeterminateAnim?.cancel();
|
|
78
|
+
const percent = this.percent();
|
|
79
|
+
if (this.#previousPercent === null) {
|
|
80
|
+
this.#previousPercent = percent;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (percent === this.#previousPercent) return;
|
|
84
|
+
|
|
85
|
+
this.#fillAnim?.cancel();
|
|
86
|
+
this.#fillAnim = el.animate(
|
|
87
|
+
[{ width: `${this.#previousPercent}%` }, { width: `${percent}%` }],
|
|
88
|
+
{ duration: DURATION.slow, easing: EASING.standard, fill: 'forwards' },
|
|
89
|
+
);
|
|
90
|
+
this.#previousPercent = percent;
|
|
91
|
+
/* v8 ignore stop */
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected readonly percent = computed(() => {
|
|
96
|
+
const m = this.max();
|
|
97
|
+
if (m <= 0) return 0;
|
|
98
|
+
return Math.min(100, Math.max(0, (this.value() / m) * 100));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
protected readonly trackClass = computed(() =>
|
|
102
|
+
cn(
|
|
103
|
+
// bg-muted (solid, theme-aware) instead of bg-primary/20: opacity
|
|
104
|
+
// modifiers on semantic colors don't work on Lynx (colors are opaque
|
|
105
|
+
// rgba values with no separable channels).
|
|
106
|
+
'h-2 w-full rounded-full bg-muted overflow-hidden',
|
|
107
|
+
this.userClass(),
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
protected readonly fillClass = computed(() =>
|
|
112
|
+
cn('h-full rounded-full bg-primary'),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
protected readonly fillStyle = computed(() =>
|
|
116
|
+
this.indeterminate() ? 'width: 40%;' : `width: ${this.percent()}%;`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiRadioGroup, UiRadioGroupItem } from './radio-group';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Component, signal } 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, vi } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiRadioGroup, UiRadioGroupItem } from './radio-group';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiRadioGroup, UiRadioGroupItem, LYNX_ELEMENTS],
|
|
11
|
+
template: `
|
|
12
|
+
<ui-radio-group [(value)]="value">
|
|
13
|
+
<ui-radio-group-item value="a">A</ui-radio-group-item>
|
|
14
|
+
<ui-radio-group-item value="b">B</ui-radio-group-item>
|
|
15
|
+
</ui-radio-group>
|
|
16
|
+
`,
|
|
17
|
+
})
|
|
18
|
+
class RadioGroupHost {
|
|
19
|
+
value = 'a';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('UiRadioGroup', () => {
|
|
23
|
+
it('renders each radio item', async () => {
|
|
24
|
+
const { getByText } = await render(RadioGroupHost);
|
|
25
|
+
expect(getByText('A')).toBeTruthy();
|
|
26
|
+
expect(getByText('B')).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('select() updates the model and emits `changed`', async () => {
|
|
30
|
+
const { componentRef } = await render(UiRadioGroup);
|
|
31
|
+
const inst = componentRef.instance as UiRadioGroup;
|
|
32
|
+
const emitted: string[] = [];
|
|
33
|
+
inst.changed.subscribe((v) => emitted.push(v));
|
|
34
|
+
inst.select('a');
|
|
35
|
+
expect(inst.value()).toBe('a');
|
|
36
|
+
inst.select('b');
|
|
37
|
+
expect(inst.value()).toBe('b');
|
|
38
|
+
expect(emitted).toEqual(['a', 'b']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('select() is a no-op when disabled', async () => {
|
|
42
|
+
const { componentRef } = await render(UiRadioGroup);
|
|
43
|
+
const inst = componentRef.instance as UiRadioGroup;
|
|
44
|
+
setInputSignal(inst.disabled, true);
|
|
45
|
+
const emitted = vi.fn();
|
|
46
|
+
inst.changed.subscribe(emitted);
|
|
47
|
+
inst.select('x');
|
|
48
|
+
expect(inst.value()).toBe('');
|
|
49
|
+
expect(emitted).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('UiRadioGroupItem', () => {
|
|
54
|
+
it('select() delegates to the parent group', async () => {
|
|
55
|
+
const parentResult = await render(UiRadioGroup);
|
|
56
|
+
const parent = parentResult.componentRef.instance as UiRadioGroup;
|
|
57
|
+
|
|
58
|
+
const itemResult = await render(UiRadioGroupItem, {
|
|
59
|
+
providers: [{ provide: UiRadioGroup, useValue: parent }],
|
|
60
|
+
});
|
|
61
|
+
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
62
|
+
setInputSignal(item.itemValue, 'a');
|
|
63
|
+
await waitForUpdate();
|
|
64
|
+
|
|
65
|
+
item.select();
|
|
66
|
+
expect(parent.value()).toBe('a');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('select() is a no-op when the parent group is disabled', async () => {
|
|
70
|
+
const parentResult = await render(UiRadioGroup);
|
|
71
|
+
const parent = parentResult.componentRef.instance as UiRadioGroup;
|
|
72
|
+
setInputSignal(parent.disabled, true);
|
|
73
|
+
|
|
74
|
+
const itemResult = await render(UiRadioGroupItem, {
|
|
75
|
+
providers: [{ provide: UiRadioGroup, useValue: parent }],
|
|
76
|
+
});
|
|
77
|
+
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
78
|
+
setInputSignal(item.itemValue, 'a');
|
|
79
|
+
await waitForUpdate();
|
|
80
|
+
|
|
81
|
+
item.select();
|
|
82
|
+
expect(parent.value()).toBe('');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('effect runs the popIn / popOut animation on selection change', async () => {
|
|
86
|
+
// The effect skips the first run (initial mount) then swaps between popIn
|
|
87
|
+
// (select) and popOut (deselect). Exercise both branches by toggling the
|
|
88
|
+
// parent's value across two mounted items.
|
|
89
|
+
// JIT + jsdom does not populate the `#dot` viewChild query, so
|
|
90
|
+
// monkey-patch dotRef with a signal returning a fake ElementRef so the
|
|
91
|
+
// `if (!el || previousSelected === undefined)` guard doesn't short-circuit.
|
|
92
|
+
const parentResult = await render(UiRadioGroup);
|
|
93
|
+
const parent = parentResult.componentRef.instance as UiRadioGroup;
|
|
94
|
+
|
|
95
|
+
const itemResult = await render(UiRadioGroupItem, {
|
|
96
|
+
providers: [{ provide: UiRadioGroup, useValue: parent }],
|
|
97
|
+
});
|
|
98
|
+
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
99
|
+
setInputSignal(item.itemValue, 'a');
|
|
100
|
+
const view = itemResult.container.querySelector('view') as Element;
|
|
101
|
+
(item as unknown as { dotRef: unknown }).dotRef = signal({
|
|
102
|
+
nativeElement: view,
|
|
103
|
+
});
|
|
104
|
+
await waitForUpdate();
|
|
105
|
+
|
|
106
|
+
// Effect first run initializes `#previousSelected`, then returns.
|
|
107
|
+
// Select => popIn branch.
|
|
108
|
+
parent.value.set('a');
|
|
109
|
+
await waitForUpdate();
|
|
110
|
+
// Deselect => popOut branch.
|
|
111
|
+
parent.value.set('b');
|
|
112
|
+
await waitForUpdate();
|
|
113
|
+
expect(parent.value()).toBe('b');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
output,
|
|
11
|
+
viewChild,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
14
|
+
|
|
15
|
+
import { type AnimationHandle, popIn, popOut } from '../../utils/animate';
|
|
16
|
+
import { cn } from '../../utils/cn';
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'ui-radio-group',
|
|
20
|
+
standalone: true,
|
|
21
|
+
imports: [LYNX_ELEMENTS],
|
|
22
|
+
encapsulation: ViewEncapsulation.None,
|
|
23
|
+
template: `
|
|
24
|
+
<view [class]="containerClass()">
|
|
25
|
+
<ng-content />
|
|
26
|
+
</view>
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
export class UiRadioGroup {
|
|
30
|
+
readonly value = model<string>('');
|
|
31
|
+
readonly disabled = input(false);
|
|
32
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
33
|
+
|
|
34
|
+
readonly changed = output<string>();
|
|
35
|
+
|
|
36
|
+
protected readonly containerClass = computed(() =>
|
|
37
|
+
cn('flex-col gap-2 flex', this.userClass()),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
select(value: string): void {
|
|
41
|
+
if (this.disabled()) return;
|
|
42
|
+
this.value.set(value);
|
|
43
|
+
this.changed.emit(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Component({
|
|
48
|
+
selector: 'ui-radio-group-item',
|
|
49
|
+
standalone: true,
|
|
50
|
+
imports: [LYNX_ELEMENTS],
|
|
51
|
+
encapsulation: ViewEncapsulation.None,
|
|
52
|
+
template: `
|
|
53
|
+
<view [class]="itemClass()" (bindtap)="select()">
|
|
54
|
+
<view [class]="circleClass()">
|
|
55
|
+
<view
|
|
56
|
+
#dot
|
|
57
|
+
[class]="dotClass()"
|
|
58
|
+
[style]="
|
|
59
|
+
isSelected() ? 'opacity: 1;' : 'opacity: 0; transform: scale(0.6);'
|
|
60
|
+
"
|
|
61
|
+
/>
|
|
62
|
+
</view>
|
|
63
|
+
<text [class]="labelClass()"><ng-content /></text>
|
|
64
|
+
</view>
|
|
65
|
+
`,
|
|
66
|
+
})
|
|
67
|
+
export class UiRadioGroupItem {
|
|
68
|
+
readonly #group = inject(UiRadioGroup);
|
|
69
|
+
|
|
70
|
+
readonly itemValue = input.required<string>({ alias: 'value' });
|
|
71
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
72
|
+
|
|
73
|
+
readonly dotRef = viewChild<ElementRef>('dot');
|
|
74
|
+
#dotAnim?: AnimationHandle;
|
|
75
|
+
#previousSelected?: boolean;
|
|
76
|
+
|
|
77
|
+
protected readonly isSelected = computed(
|
|
78
|
+
() => this.#group.value() === this.itemValue(),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
constructor() {
|
|
82
|
+
// Same guard pattern as UiCheckbox: `#previousSelected === undefined` skips
|
|
83
|
+
// the first run so a pre-selected item doesn't animate in on mount.
|
|
84
|
+
// The equality guard prevents duplicate animations if the effect re-runs.
|
|
85
|
+
effect(() => {
|
|
86
|
+
const selected = this.isSelected();
|
|
87
|
+
const el = this.dotRef()?.nativeElement;
|
|
88
|
+
if (!el || this.#previousSelected === undefined) {
|
|
89
|
+
this.#previousSelected = selected;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Defensive early return: with only `isSelected()` as a reactive input,
|
|
93
|
+
// Angular's computed equality dedupes same-value emissions so the effect
|
|
94
|
+
// never re-runs with an unchanged `selected`. Kept for safety on-device.
|
|
95
|
+
/* v8 ignore next 1 */
|
|
96
|
+
if (selected === this.#previousSelected) return;
|
|
97
|
+
this.#previousSelected = selected;
|
|
98
|
+
|
|
99
|
+
this.#dotAnim?.cancel();
|
|
100
|
+
this.#dotAnim = selected ? popIn(el) : popOut(el);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected readonly itemClass = computed(() =>
|
|
105
|
+
cn(
|
|
106
|
+
'h-11 flex-row items-center gap-3 flex',
|
|
107
|
+
this.#group.disabled() && 'opacity-50',
|
|
108
|
+
this.userClass(),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
protected readonly circleClass = computed(() =>
|
|
113
|
+
cn(
|
|
114
|
+
'h-5 w-5 items-center rounded-full border flex justify-center',
|
|
115
|
+
this.isSelected() ? 'border-primary' : 'border-primary',
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
protected readonly dotClass = computed(() =>
|
|
120
|
+
cn('h-2.5 w-2.5 rounded-full bg-primary'),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
protected readonly labelClass = computed(() => cn('text-sm text-foreground'));
|
|
124
|
+
|
|
125
|
+
select(): void {
|
|
126
|
+
if (this.#group.disabled()) return;
|
|
127
|
+
this.#group.select(this.itemValue());
|
|
128
|
+
}
|
|
129
|
+
}
|