@blotch/dolan 0.0.0-stage → 22.2.1
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,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AnimationHandle,
|
|
15
|
+
pressDown,
|
|
16
|
+
pressRelease,
|
|
17
|
+
} from '../../utils/animate';
|
|
18
|
+
import { cn } from '../../utils/cn';
|
|
19
|
+
import { UiSpinner } from '../spinner/spinner';
|
|
20
|
+
|
|
21
|
+
export const buttonVariants = cva(
|
|
22
|
+
'items-center rounded-md flex justify-center',
|
|
23
|
+
{
|
|
24
|
+
variants: {
|
|
25
|
+
variant: {
|
|
26
|
+
default: 'bg-primary',
|
|
27
|
+
destructive: 'bg-destructive',
|
|
28
|
+
outline: 'border border-border bg-background',
|
|
29
|
+
secondary: 'bg-secondary',
|
|
30
|
+
ghost: 'bg-transparent',
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
default: 'h-10 px-4 py-2',
|
|
34
|
+
sm: 'h-9 rounded-md px-3',
|
|
35
|
+
lg: 'h-11 rounded-md px-8',
|
|
36
|
+
icon: 'h-10 w-10',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
variant: 'default',
|
|
41
|
+
size: 'default',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const buttonTextVariants = cva('font-medium', {
|
|
47
|
+
variants: {
|
|
48
|
+
variant: {
|
|
49
|
+
default: 'text-primary-foreground',
|
|
50
|
+
destructive: 'text-destructive-foreground',
|
|
51
|
+
outline: 'text-foreground',
|
|
52
|
+
secondary: 'text-secondary-foreground',
|
|
53
|
+
ghost: 'text-foreground',
|
|
54
|
+
},
|
|
55
|
+
size: {
|
|
56
|
+
default: 'text-sm',
|
|
57
|
+
sm: 'text-xs',
|
|
58
|
+
lg: 'text-base',
|
|
59
|
+
icon: 'text-sm',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
defaultVariants: {
|
|
63
|
+
variant: 'default',
|
|
64
|
+
size: 'default',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type ButtonVariant = NonNullable<
|
|
69
|
+
VariantProps<typeof buttonVariants>['variant']
|
|
70
|
+
>;
|
|
71
|
+
export type ButtonSize = NonNullable<
|
|
72
|
+
VariantProps<typeof buttonVariants>['size']
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
@Component({
|
|
76
|
+
selector: 'ui-button',
|
|
77
|
+
standalone: true,
|
|
78
|
+
imports: [LYNX_ELEMENTS, UiSpinner],
|
|
79
|
+
encapsulation: ViewEncapsulation.None,
|
|
80
|
+
template: `
|
|
81
|
+
<!-- Press feedback binds BOTH touch and mouse events on purpose. Native
|
|
82
|
+
(iOS/Android) only ever fires touch; Lynx web runs in the browser, where
|
|
83
|
+
a cursor press fires mousedown/up and NEVER touchstart — so touch-only
|
|
84
|
+
bindings gave zero visual feedback on web while (bindtap) still fired the
|
|
85
|
+
action (a "the button works but doesn't react" split). Binding both makes
|
|
86
|
+
the press-and-hold identical on every platform; mouse events are inert on
|
|
87
|
+
touch devices, so there's no double-fire. The scale animation itself lives
|
|
88
|
+
in pressDown/pressRelease — see their JSDoc for why it's a CSS transition
|
|
89
|
+
(which serializes to the DOM on Lynx web) and not el.animate() (whose Web
|
|
90
|
+
Animation runs on web-core's offscreen node and never reaches the screen). -->
|
|
91
|
+
<view
|
|
92
|
+
#container
|
|
93
|
+
[class]="containerClass()"
|
|
94
|
+
(bindtouchstart)="onPressStart()"
|
|
95
|
+
(bindtouchend)="onPressEnd()"
|
|
96
|
+
(bindtouchcancel)="onPressCancel()"
|
|
97
|
+
(bindmousedown)="onPressStart()"
|
|
98
|
+
(bindmouseup)="onPressEnd()"
|
|
99
|
+
(bindmouseleave)="onPressCancel()"
|
|
100
|
+
(bindtap)="onTap()"
|
|
101
|
+
>
|
|
102
|
+
@if (loading()) {
|
|
103
|
+
<ui-spinner [size]="spinnerSize()" />
|
|
104
|
+
} @else {
|
|
105
|
+
<text [class]="labelClass()"><ng-content /></text>
|
|
106
|
+
}
|
|
107
|
+
</view>
|
|
108
|
+
`,
|
|
109
|
+
})
|
|
110
|
+
export class UiButton {
|
|
111
|
+
readonly variant = input<ButtonVariant>('default');
|
|
112
|
+
readonly size = input<ButtonSize>('default');
|
|
113
|
+
readonly disabled = input(false);
|
|
114
|
+
readonly loading = input(false);
|
|
115
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
116
|
+
|
|
117
|
+
readonly pressed = output<void>();
|
|
118
|
+
|
|
119
|
+
readonly containerRef = viewChild<ElementRef>('container');
|
|
120
|
+
#pressAnim?: AnimationHandle;
|
|
121
|
+
|
|
122
|
+
protected readonly containerClass = computed(() =>
|
|
123
|
+
cn(
|
|
124
|
+
buttonVariants({ variant: this.variant(), size: this.size() }),
|
|
125
|
+
(this.disabled() || this.loading()) && 'opacity-50',
|
|
126
|
+
this.userClass(),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
protected readonly labelClass = computed(() =>
|
|
131
|
+
cn(buttonTextVariants({ variant: this.variant(), size: this.size() })),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
protected readonly spinnerSize = computed(() => {
|
|
135
|
+
const s = this.size();
|
|
136
|
+
return s === 'sm' ? ('xs' as const) : ('sm' as const);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
protected onPressStart(): void {
|
|
140
|
+
if (this.disabled() || this.loading()) return;
|
|
141
|
+
// pressDown/pressRelease drive el.setStyle() based CSS transitions on the
|
|
142
|
+
// container ref. In the JIT test harness the view-child isn't reliably
|
|
143
|
+
// wired up (see the label spec note), and Lynx web renderer's press
|
|
144
|
+
// feedback is verified end-to-end on-device.
|
|
145
|
+
/* v8 ignore next 2 */
|
|
146
|
+
this.#pressAnim?.cancel();
|
|
147
|
+
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected onPressEnd(): void {
|
|
151
|
+
if (this.disabled() || this.loading()) return;
|
|
152
|
+
/* v8 ignore next 2 */
|
|
153
|
+
this.#pressAnim?.cancel();
|
|
154
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected onPressCancel(): void {
|
|
158
|
+
// Also restore scale on cancel — without this, a canceled touch (e.g. scroll
|
|
159
|
+
// gesture taking over) leaves the button stuck in its pressed-down state.
|
|
160
|
+
/* v8 ignore next 2 */
|
|
161
|
+
this.#pressAnim?.cancel();
|
|
162
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected onTap(): void {
|
|
166
|
+
if (!this.disabled() && !this.loading()) {
|
|
167
|
+
this.pressed.emit();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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, vi } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiButtonGroup, UiButtonGroupItem } from './button-group';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiButtonGroup, UiButtonGroupItem, LYNX_ELEMENTS],
|
|
11
|
+
template: `
|
|
12
|
+
<ui-button-group [orientation]="orientation">
|
|
13
|
+
<ui-button-group-item (pressed)="onPressed()"
|
|
14
|
+
><text>A</text></ui-button-group-item
|
|
15
|
+
>
|
|
16
|
+
<ui-button-group-item [disabled]="disabled" (pressed)="onPressed()"
|
|
17
|
+
><text>B</text></ui-button-group-item
|
|
18
|
+
>
|
|
19
|
+
</ui-button-group>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
class ButtonGroupHost {
|
|
23
|
+
orientation: 'horizontal' | 'vertical' = 'horizontal';
|
|
24
|
+
disabled = false;
|
|
25
|
+
onPressed = () => {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('UiButtonGroup (render)', () => {
|
|
29
|
+
it('renders horizontally by default', async () => {
|
|
30
|
+
const { container } = await render(ButtonGroupHost);
|
|
31
|
+
expect(container).toBeTruthy();
|
|
32
|
+
const flexRow = container.querySelector('.flex-row');
|
|
33
|
+
expect(flexRow).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('switches to vertical layout when orientation is flipped', async () => {
|
|
37
|
+
const { componentRef, container } = await render(UiButtonGroup);
|
|
38
|
+
setInputSignal(
|
|
39
|
+
(componentRef.instance as UiButtonGroup).orientation,
|
|
40
|
+
'vertical',
|
|
41
|
+
);
|
|
42
|
+
await waitForUpdate();
|
|
43
|
+
expect(container.querySelector('.flex-col')).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies a user class from the alias input', async () => {
|
|
47
|
+
const { componentRef, container } = await render(UiButtonGroup);
|
|
48
|
+
setInputSignal(
|
|
49
|
+
(componentRef.instance as UiButtonGroup).userClass,
|
|
50
|
+
'w-full',
|
|
51
|
+
);
|
|
52
|
+
await waitForUpdate();
|
|
53
|
+
const outer = container.querySelector('view');
|
|
54
|
+
expect(outer?.getAttribute('class')).toContain('w-full');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('UiButtonGroupItem', () => {
|
|
59
|
+
const renderItem = async (
|
|
60
|
+
variant: 'default' | 'outline' = 'default',
|
|
61
|
+
size: 'default' | 'sm' | 'lg' = 'default',
|
|
62
|
+
disabledGroup = false,
|
|
63
|
+
) => {
|
|
64
|
+
const parentResult = await render(UiButtonGroup);
|
|
65
|
+
const parent = parentResult.componentRef.instance as UiButtonGroup;
|
|
66
|
+
setInputSignal(parent.variant, variant);
|
|
67
|
+
setInputSignal(parent.size, size);
|
|
68
|
+
setInputSignal(parent.disabled, disabledGroup);
|
|
69
|
+
await waitForUpdate();
|
|
70
|
+
|
|
71
|
+
const itemResult = await render(UiButtonGroupItem, {
|
|
72
|
+
providers: [{ provide: UiButtonGroup, useValue: parent }],
|
|
73
|
+
});
|
|
74
|
+
await waitForUpdate();
|
|
75
|
+
return { parent, itemResult };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
it('onTap emits `pressed` when neither the group nor the item is disabled', async () => {
|
|
79
|
+
const { itemResult } = await renderItem();
|
|
80
|
+
const inst = itemResult.componentRef
|
|
81
|
+
.instance as unknown as UiButtonGroupItem & {
|
|
82
|
+
onTap: () => void;
|
|
83
|
+
};
|
|
84
|
+
const emitted = vi.fn();
|
|
85
|
+
inst.pressed.subscribe(emitted);
|
|
86
|
+
inst.onTap();
|
|
87
|
+
expect(emitted).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('onTap is a no-op when the group is disabled', async () => {
|
|
91
|
+
const { itemResult } = await renderItem('default', 'default', true);
|
|
92
|
+
const inst = itemResult.componentRef
|
|
93
|
+
.instance as unknown as UiButtonGroupItem & {
|
|
94
|
+
onTap: () => void;
|
|
95
|
+
};
|
|
96
|
+
const emitted = vi.fn();
|
|
97
|
+
inst.pressed.subscribe(emitted);
|
|
98
|
+
inst.onTap();
|
|
99
|
+
expect(emitted).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('onTap is a no-op when the item itself is disabled', async () => {
|
|
103
|
+
const { itemResult } = await renderItem();
|
|
104
|
+
const inst = itemResult.componentRef
|
|
105
|
+
.instance as unknown as UiButtonGroupItem & {
|
|
106
|
+
onTap: () => void;
|
|
107
|
+
};
|
|
108
|
+
setInputSignal(inst.disabled, true);
|
|
109
|
+
await waitForUpdate();
|
|
110
|
+
const emitted = vi.fn();
|
|
111
|
+
inst.pressed.subscribe(emitted);
|
|
112
|
+
inst.onTap();
|
|
113
|
+
expect(emitted).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('press handlers run cleanly on both enabled and disabled paths', async () => {
|
|
117
|
+
const { itemResult } = await renderItem();
|
|
118
|
+
const inst = itemResult.componentRef.instance as unknown as {
|
|
119
|
+
onPressStart: () => void;
|
|
120
|
+
onPressEnd: () => void;
|
|
121
|
+
onPressCancel: () => void;
|
|
122
|
+
disabled: unknown;
|
|
123
|
+
};
|
|
124
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
125
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
126
|
+
expect(() => inst.onPressCancel()).not.toThrow();
|
|
127
|
+
setInputSignal((inst as unknown as UiButtonGroupItem).disabled, true);
|
|
128
|
+
await waitForUpdate();
|
|
129
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
130
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('renders label with `outline` group variant classes', async () => {
|
|
134
|
+
// Covers the `outline` branch of both buttonGroupItemVariants and
|
|
135
|
+
// buttonGroupTextVariants.
|
|
136
|
+
const { itemResult } = await renderItem('outline', 'lg');
|
|
137
|
+
const view = itemResult.container.querySelector('view');
|
|
138
|
+
expect(view?.getAttribute('class')).toContain('bg-background');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('renders with size sm variant classes', async () => {
|
|
142
|
+
const { itemResult } = await renderItem('default', 'sm');
|
|
143
|
+
const view = itemResult.container.querySelector('view');
|
|
144
|
+
expect(view?.getAttribute('class')).toContain('h-9');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Angular so importing the component module doesn't require the framework
|
|
4
|
+
// runtime. Only the exported `cva` variant fn is exercised here; the class field
|
|
5
|
+
// initializers (input/output/viewChild/inject) never run because we never
|
|
6
|
+
// construct the components.
|
|
7
|
+
vi.mock('@angular/core', () => ({
|
|
8
|
+
Component: () => () => {},
|
|
9
|
+
ViewEncapsulation: { None: 0 },
|
|
10
|
+
computed: () => () => {},
|
|
11
|
+
inject: () => ({}),
|
|
12
|
+
input: () => () => {},
|
|
13
|
+
output: () => () => {},
|
|
14
|
+
viewChild: () => () => {},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@blotch/angular-lynx', () => ({
|
|
18
|
+
LYNX_ELEMENTS: [],
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { buttonGroupItemVariants } = await import('./button-group');
|
|
22
|
+
|
|
23
|
+
describe('buttonGroupItemVariants', () => {
|
|
24
|
+
it('returns default variant and size classes', () => {
|
|
25
|
+
const result = buttonGroupItemVariants();
|
|
26
|
+
expect(result).toContain('bg-secondary');
|
|
27
|
+
expect(result).toContain('h-10');
|
|
28
|
+
expect(result).toContain('px-4');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns outline variant classes', () => {
|
|
32
|
+
const result = buttonGroupItemVariants({ variant: 'outline' });
|
|
33
|
+
expect(result).toContain('bg-background');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns small size classes', () => {
|
|
37
|
+
const result = buttonGroupItemVariants({ size: 'sm' });
|
|
38
|
+
expect(result).toContain('h-9');
|
|
39
|
+
expect(result).toContain('px-3');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns large size classes', () => {
|
|
43
|
+
const result = buttonGroupItemVariants({ size: 'lg' });
|
|
44
|
+
expect(result).toContain('h-11');
|
|
45
|
+
expect(result).toContain('px-6');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('always includes base layout classes', () => {
|
|
49
|
+
const result = buttonGroupItemVariants({ variant: 'outline', size: 'lg' });
|
|
50
|
+
expect(result).toContain('flex');
|
|
51
|
+
expect(result).toContain('items-center');
|
|
52
|
+
expect(result).toContain('justify-center');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
inject,
|
|
7
|
+
input,
|
|
8
|
+
output,
|
|
9
|
+
viewChild,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
12
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AnimationHandle,
|
|
16
|
+
pressDown,
|
|
17
|
+
pressRelease,
|
|
18
|
+
} from '../../utils/animate';
|
|
19
|
+
import { cn } from '../../utils/cn';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-item styling. Corners are intentionally left square here: the group
|
|
23
|
+
* container clips the whole bar with `overflow-hidden` + `rounded-md`, so only
|
|
24
|
+
* the outer edges round off while every internal seam stays crisp. Rounding
|
|
25
|
+
* individual items by position would require `:first-child`/`:last-child`
|
|
26
|
+
* selectors, which Lynx's CSS engine does not support (no component in the
|
|
27
|
+
* library relies on positional selectors — see CLAUDE.md).
|
|
28
|
+
*/
|
|
29
|
+
export const buttonGroupItemVariants = cva('items-center flex justify-center', {
|
|
30
|
+
variants: {
|
|
31
|
+
variant: {
|
|
32
|
+
// Filled segments — the whole bar reads as a solid control.
|
|
33
|
+
default: 'bg-secondary',
|
|
34
|
+
// Neutral segments against the app background, ringed by the container.
|
|
35
|
+
outline: 'bg-background',
|
|
36
|
+
},
|
|
37
|
+
size: {
|
|
38
|
+
default: 'h-10 px-4',
|
|
39
|
+
sm: 'h-9 px-3',
|
|
40
|
+
lg: 'h-11 px-6',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
variant: 'default',
|
|
45
|
+
size: 'default',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const buttonGroupTextVariants = cva('font-medium', {
|
|
50
|
+
variants: {
|
|
51
|
+
variant: {
|
|
52
|
+
default: 'text-secondary-foreground',
|
|
53
|
+
outline: 'text-foreground',
|
|
54
|
+
},
|
|
55
|
+
size: {
|
|
56
|
+
default: 'text-sm',
|
|
57
|
+
sm: 'text-xs',
|
|
58
|
+
lg: 'text-base',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: {
|
|
62
|
+
variant: 'default',
|
|
63
|
+
size: 'default',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type ButtonGroupVariant = NonNullable<
|
|
68
|
+
VariantProps<typeof buttonGroupItemVariants>['variant']
|
|
69
|
+
>;
|
|
70
|
+
export type ButtonGroupSize = NonNullable<
|
|
71
|
+
VariantProps<typeof buttonGroupItemVariants>['size']
|
|
72
|
+
>;
|
|
73
|
+
export type ButtonGroupOrientation = 'horizontal' | 'vertical';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A row (or column) of connected action buttons — a toolbar of related
|
|
77
|
+
* commands rendered as a single, seamless control.
|
|
78
|
+
*
|
|
79
|
+
* The group owns the shared appearance (`variant`, `size`, `orientation`,
|
|
80
|
+
* `disabled`) and each `<ui-button-group-item>` reads it back via DI, mirroring
|
|
81
|
+
* the `UiToggleGroup`/`UiToggleGroupItem` pattern. Unlike a toggle group this
|
|
82
|
+
* carries no selection state: every item is an independent action that emits
|
|
83
|
+
* `pressed`.
|
|
84
|
+
*/
|
|
85
|
+
@Component({
|
|
86
|
+
selector: 'ui-button-group',
|
|
87
|
+
standalone: true,
|
|
88
|
+
imports: [LYNX_ELEMENTS],
|
|
89
|
+
encapsulation: ViewEncapsulation.None,
|
|
90
|
+
template: `
|
|
91
|
+
<view [class]="containerClass()">
|
|
92
|
+
<ng-content />
|
|
93
|
+
</view>
|
|
94
|
+
`,
|
|
95
|
+
})
|
|
96
|
+
export class UiButtonGroup {
|
|
97
|
+
readonly orientation = input<ButtonGroupOrientation>('horizontal');
|
|
98
|
+
readonly variant = input<ButtonGroupVariant>('default');
|
|
99
|
+
readonly size = input<ButtonGroupSize>('default');
|
|
100
|
+
readonly disabled = input(false);
|
|
101
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
102
|
+
|
|
103
|
+
protected readonly containerClass = computed(() =>
|
|
104
|
+
cn(
|
|
105
|
+
// The dividers between items are the group's own `bg-border` showing
|
|
106
|
+
// through a 1px `gap-px` seam — no per-item borders needed, which keeps
|
|
107
|
+
// items position-agnostic. `overflow-hidden` + `rounded-md` then clip the
|
|
108
|
+
// square item corners so the bar reads as one rounded control.
|
|
109
|
+
'gap-px rounded-md border border-border bg-border flex overflow-hidden',
|
|
110
|
+
// `self-start` stops the bar stretching to fill a column parent so it
|
|
111
|
+
// hugs its buttons like a real toolbar. Pass `class="w-full"` to override.
|
|
112
|
+
'self-start',
|
|
113
|
+
this.orientation() === 'vertical' ? 'flex-col' : 'flex-row',
|
|
114
|
+
this.userClass(),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A single pressable action inside a `<ui-button-group>`. Inherits appearance
|
|
121
|
+
* from the parent group; set `disabled` per-item to disable just this action.
|
|
122
|
+
*/
|
|
123
|
+
@Component({
|
|
124
|
+
selector: 'ui-button-group-item',
|
|
125
|
+
standalone: true,
|
|
126
|
+
imports: [LYNX_ELEMENTS],
|
|
127
|
+
encapsulation: ViewEncapsulation.None,
|
|
128
|
+
template: `
|
|
129
|
+
<view
|
|
130
|
+
#container
|
|
131
|
+
[class]="containerClass()"
|
|
132
|
+
(bindtouchstart)="onPressStart()"
|
|
133
|
+
(bindtouchend)="onPressEnd()"
|
|
134
|
+
(bindtouchcancel)="onPressCancel()"
|
|
135
|
+
(bindmousedown)="onPressStart()"
|
|
136
|
+
(bindmouseup)="onPressEnd()"
|
|
137
|
+
(bindmouseleave)="onPressCancel()"
|
|
138
|
+
(bindtap)="onTap()"
|
|
139
|
+
>
|
|
140
|
+
<text [class]="labelClass()"><ng-content /></text>
|
|
141
|
+
</view>
|
|
142
|
+
`,
|
|
143
|
+
})
|
|
144
|
+
export class UiButtonGroupItem {
|
|
145
|
+
readonly #group = inject(UiButtonGroup);
|
|
146
|
+
|
|
147
|
+
readonly disabled = input(false);
|
|
148
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
149
|
+
|
|
150
|
+
readonly pressed = output<void>();
|
|
151
|
+
|
|
152
|
+
readonly containerRef = viewChild<ElementRef>('container');
|
|
153
|
+
#pressAnim?: AnimationHandle;
|
|
154
|
+
|
|
155
|
+
// Disabled when either the whole group or this specific item is disabled.
|
|
156
|
+
protected readonly isDisabled = computed(
|
|
157
|
+
() => this.#group.disabled() || this.disabled(),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
protected readonly containerClass = computed(() =>
|
|
161
|
+
cn(
|
|
162
|
+
buttonGroupItemVariants({
|
|
163
|
+
variant: this.#group.variant(),
|
|
164
|
+
size: this.#group.size(),
|
|
165
|
+
}),
|
|
166
|
+
this.isDisabled() && 'opacity-50',
|
|
167
|
+
this.userClass(),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
protected readonly labelClass = computed(() =>
|
|
172
|
+
cn(
|
|
173
|
+
buttonGroupTextVariants({
|
|
174
|
+
variant: this.#group.variant(),
|
|
175
|
+
size: this.#group.size(),
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
protected onPressStart(): void {
|
|
181
|
+
if (this.isDisabled()) return;
|
|
182
|
+
// See UiButton — press animations use el.setStyle-driven transitions that
|
|
183
|
+
// are inert in the JIT test harness. Verified on-device.
|
|
184
|
+
/* v8 ignore next 2 */
|
|
185
|
+
this.#pressAnim?.cancel();
|
|
186
|
+
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected onPressEnd(): void {
|
|
190
|
+
if (this.isDisabled()) return;
|
|
191
|
+
/* v8 ignore next 2 */
|
|
192
|
+
this.#pressAnim?.cancel();
|
|
193
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected onPressCancel(): void {
|
|
197
|
+
// Restore scale on cancel too — otherwise a touch stolen by a scroll
|
|
198
|
+
// gesture leaves the item stuck in its pressed-down state.
|
|
199
|
+
/* v8 ignore next 2 */
|
|
200
|
+
this.#pressAnim?.cancel();
|
|
201
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
protected onTap(): void {
|
|
205
|
+
if (!this.isDisabled()) {
|
|
206
|
+
this.pressed.emit();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|