@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.
Files changed (273) hide show
  1. package/dist/commands/add.d.ts +22 -0
  2. package/dist/commands/add.js +132 -0
  3. package/dist/commands/add.js.map +1 -0
  4. package/dist/commands/add.test.d.ts +1 -0
  5. package/dist/commands/add.test.js +209 -0
  6. package/dist/commands/add.test.js.map +1 -0
  7. package/dist/commands/diff.d.ts +13 -0
  8. package/dist/commands/diff.js +155 -0
  9. package/dist/commands/diff.js.map +1 -0
  10. package/dist/commands/diff.test.d.ts +1 -0
  11. package/dist/commands/diff.test.js +327 -0
  12. package/dist/commands/diff.test.js.map +1 -0
  13. package/dist/commands/doctor.d.ts +15 -0
  14. package/dist/commands/doctor.js +248 -0
  15. package/dist/commands/doctor.js.map +1 -0
  16. package/dist/commands/doctor.test.d.ts +1 -0
  17. package/dist/commands/doctor.test.js +301 -0
  18. package/dist/commands/doctor.test.js.map +1 -0
  19. package/dist/commands/eject.d.ts +11 -0
  20. package/dist/commands/eject.js +55 -0
  21. package/dist/commands/eject.js.map +1 -0
  22. package/dist/commands/eject.test.d.ts +1 -0
  23. package/dist/commands/eject.test.js +151 -0
  24. package/dist/commands/eject.test.js.map +1 -0
  25. package/dist/commands/info.d.ts +1 -0
  26. package/dist/commands/info.js +73 -0
  27. package/dist/commands/info.js.map +1 -0
  28. package/dist/commands/info.test.d.ts +1 -0
  29. package/dist/commands/info.test.js +143 -0
  30. package/dist/commands/info.test.js.map +1 -0
  31. package/dist/commands/init.d.ts +1 -0
  32. package/dist/commands/init.js +98 -0
  33. package/dist/commands/init.js.map +1 -0
  34. package/dist/commands/init.test.d.ts +1 -0
  35. package/dist/commands/init.test.js +154 -0
  36. package/dist/commands/init.test.js.map +1 -0
  37. package/dist/commands/list.d.ts +16 -0
  38. package/dist/commands/list.js +124 -0
  39. package/dist/commands/list.js.map +1 -0
  40. package/dist/commands/list.test.d.ts +1 -0
  41. package/dist/commands/list.test.js +285 -0
  42. package/dist/commands/list.test.js.map +1 -0
  43. package/dist/commands/outdated.d.ts +13 -0
  44. package/dist/commands/outdated.js +116 -0
  45. package/dist/commands/outdated.js.map +1 -0
  46. package/dist/commands/outdated.test.d.ts +1 -0
  47. package/dist/commands/outdated.test.js +208 -0
  48. package/dist/commands/outdated.test.js.map +1 -0
  49. package/dist/commands/remove.d.ts +18 -0
  50. package/dist/commands/remove.js +161 -0
  51. package/dist/commands/remove.js.map +1 -0
  52. package/dist/commands/remove.test.d.ts +1 -0
  53. package/dist/commands/remove.test.js +361 -0
  54. package/dist/commands/remove.test.js.map +1 -0
  55. package/dist/commands/theme.d.ts +1 -0
  56. package/dist/commands/theme.js +66 -0
  57. package/dist/commands/theme.js.map +1 -0
  58. package/dist/commands/theme.test.d.ts +1 -0
  59. package/dist/commands/theme.test.js +133 -0
  60. package/dist/commands/theme.test.js.map +1 -0
  61. package/dist/commands/update.d.ts +31 -0
  62. package/dist/commands/update.js +601 -0
  63. package/dist/commands/update.js.map +1 -0
  64. package/dist/commands/update.test.d.ts +1 -0
  65. package/dist/commands/update.test.js +1233 -0
  66. package/dist/commands/update.test.js.map +1 -0
  67. package/dist/config.d.ts +11 -0
  68. package/dist/config.js +19 -0
  69. package/dist/config.js.map +1 -0
  70. package/dist/config.test.d.ts +1 -0
  71. package/dist/config.test.js +48 -0
  72. package/dist/config.test.js.map +1 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +89 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/lockfile.d.ts +21 -0
  77. package/dist/lockfile.js +39 -0
  78. package/dist/lockfile.js.map +1 -0
  79. package/dist/registry.d.ts +14 -0
  80. package/dist/registry.js +85 -0
  81. package/dist/registry.js.map +1 -0
  82. package/dist/registry.test.d.ts +1 -0
  83. package/dist/registry.test.js +95 -0
  84. package/dist/registry.test.js.map +1 -0
  85. package/dist/test-utils.d.ts +16 -0
  86. package/dist/test-utils.js +65 -0
  87. package/dist/test-utils.js.map +1 -0
  88. package/dist/ui/components/accordion/accordion.test.ts +194 -0
  89. package/dist/ui/components/accordion/accordion.ts +173 -0
  90. package/dist/ui/components/accordion/index.ts +6 -0
  91. package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
  92. package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
  93. package/dist/ui/components/action-sheet/index.ts +6 -0
  94. package/dist/ui/components/alert/alert.test.ts +48 -0
  95. package/dist/ui/components/alert/alert.ts +104 -0
  96. package/dist/ui/components/alert/index.ts +1 -0
  97. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
  98. package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
  99. package/dist/ui/components/alert-dialog/index.ts +7 -0
  100. package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
  101. package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
  102. package/dist/ui/components/area-chart/area-chart.ts +349 -0
  103. package/dist/ui/components/area-chart/index.ts +6 -0
  104. package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
  105. package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
  106. package/dist/ui/components/aspect-ratio/index.ts +1 -0
  107. package/dist/ui/components/avatar/avatar.test.ts +69 -0
  108. package/dist/ui/components/avatar/avatar.ts +131 -0
  109. package/dist/ui/components/avatar/index.ts +1 -0
  110. package/dist/ui/components/badge/badge.test.ts +49 -0
  111. package/dist/ui/components/badge/badge.ts +97 -0
  112. package/dist/ui/components/badge/index.ts +1 -0
  113. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
  114. package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
  115. package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
  116. package/dist/ui/components/bar-chart/index.ts +6 -0
  117. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
  118. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
  119. package/dist/ui/components/bottom-sheet/index.ts +1 -0
  120. package/dist/ui/components/button/button-render.test.ts +113 -0
  121. package/dist/ui/components/button/button.test.ts +70 -0
  122. package/dist/ui/components/button/button.ts +170 -0
  123. package/dist/ui/components/button/index.ts +6 -0
  124. package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
  125. package/dist/ui/components/button-group/button-group.test.ts +54 -0
  126. package/dist/ui/components/button-group/button-group.ts +209 -0
  127. package/dist/ui/components/button-group/index.ts +8 -0
  128. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
  129. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
  130. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
  131. package/dist/ui/components/candlestick-chart/index.ts +7 -0
  132. package/dist/ui/components/card/card.test.ts +110 -0
  133. package/dist/ui/components/card/card.ts +162 -0
  134. package/dist/ui/components/card/index.ts +8 -0
  135. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
  136. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
  137. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
  138. package/dist/ui/components/cartesian-chart/index.ts +18 -0
  139. package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
  140. package/dist/ui/components/checkbox/checkbox.ts +96 -0
  141. package/dist/ui/components/checkbox/index.ts +1 -0
  142. package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
  143. package/dist/ui/components/collapsible/collapsible.ts +122 -0
  144. package/dist/ui/components/collapsible/index.ts +5 -0
  145. package/dist/ui/components/dialog/dialog.test.ts +157 -0
  146. package/dist/ui/components/dialog/dialog.ts +248 -0
  147. package/dist/ui/components/dialog/index.ts +7 -0
  148. package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
  149. package/dist/ui/components/empty-state/empty-state.ts +46 -0
  150. package/dist/ui/components/empty-state/index.ts +1 -0
  151. package/dist/ui/components/icon/icon.test.ts +52 -0
  152. package/dist/ui/components/icon/icon.ts +54 -0
  153. package/dist/ui/components/icon/icons.ts +569 -0
  154. package/dist/ui/components/icon/index.ts +2 -0
  155. package/dist/ui/components/input/index.ts +1 -0
  156. package/dist/ui/components/input/input.test.ts +134 -0
  157. package/dist/ui/components/input/input.ts +189 -0
  158. package/dist/ui/components/label/index.ts +1 -0
  159. package/dist/ui/components/label/label.test.ts +38 -0
  160. package/dist/ui/components/label/label.ts +24 -0
  161. package/dist/ui/components/line-chart/index.ts +6 -0
  162. package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
  163. package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
  164. package/dist/ui/components/line-chart/line-chart.ts +273 -0
  165. package/dist/ui/components/list/index.ts +1 -0
  166. package/dist/ui/components/list/list.test.ts +35 -0
  167. package/dist/ui/components/list/list.ts +71 -0
  168. package/dist/ui/components/nav-drawer/index.ts +8 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
  171. package/dist/ui/components/pagination/index.ts +1 -0
  172. package/dist/ui/components/pagination/pagination.test.ts +96 -0
  173. package/dist/ui/components/pagination/pagination.ts +147 -0
  174. package/dist/ui/components/progress/index.ts +1 -0
  175. package/dist/ui/components/progress/progress.test.ts +71 -0
  176. package/dist/ui/components/progress/progress.ts +118 -0
  177. package/dist/ui/components/radio-group/index.ts +1 -0
  178. package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
  179. package/dist/ui/components/radio-group/radio-group.ts +129 -0
  180. package/dist/ui/components/scatter-chart/index.ts +6 -0
  181. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
  182. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
  183. package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
  184. package/dist/ui/components/scroll-area/index.ts +1 -0
  185. package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
  186. package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
  187. package/dist/ui/components/select/index.ts +1 -0
  188. package/dist/ui/components/select/select.test.ts +107 -0
  189. package/dist/ui/components/select/select.ts +146 -0
  190. package/dist/ui/components/separator/index.ts +1 -0
  191. package/dist/ui/components/separator/separator.test.ts +26 -0
  192. package/dist/ui/components/separator/separator.ts +45 -0
  193. package/dist/ui/components/sheet/index.ts +7 -0
  194. package/dist/ui/components/sheet/sheet.test.ts +44 -0
  195. package/dist/ui/components/sheet/sheet.ts +114 -0
  196. package/dist/ui/components/skeleton/index.ts +1 -0
  197. package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
  198. package/dist/ui/components/skeleton/skeleton.ts +45 -0
  199. package/dist/ui/components/spinner/index.ts +1 -0
  200. package/dist/ui/components/spinner/spinner.test.ts +39 -0
  201. package/dist/ui/components/spinner/spinner.ts +83 -0
  202. package/dist/ui/components/switch/index.ts +1 -0
  203. package/dist/ui/components/switch/switch.test.ts +63 -0
  204. package/dist/ui/components/switch/switch.ts +98 -0
  205. package/dist/ui/components/tabs/index.ts +1 -0
  206. package/dist/ui/components/tabs/tabs.test.ts +199 -0
  207. package/dist/ui/components/tabs/tabs.ts +317 -0
  208. package/dist/ui/components/textarea/index.ts +1 -0
  209. package/dist/ui/components/textarea/textarea.test.ts +138 -0
  210. package/dist/ui/components/textarea/textarea.ts +221 -0
  211. package/dist/ui/components/toast/index.ts +9 -0
  212. package/dist/ui/components/toast/toast-state.test.ts +54 -0
  213. package/dist/ui/components/toast/toast-state.ts +46 -0
  214. package/dist/ui/components/toast/toast.test.ts +342 -0
  215. package/dist/ui/components/toast/toast.ts +527 -0
  216. package/dist/ui/components/toggle/index.ts +7 -0
  217. package/dist/ui/components/toggle/toggle.test.ts +186 -0
  218. package/dist/ui/components/toggle/toggle.ts +218 -0
  219. package/dist/ui/components/typography/index.ts +5 -0
  220. package/dist/ui/components/typography/typography.test.ts +55 -0
  221. package/dist/ui/components/typography/typography.ts +44 -0
  222. package/dist/ui/theme/dark.css +41 -0
  223. package/dist/ui/theme/default.css +56 -0
  224. package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
  225. package/dist/ui/theme/tailwind-plugin.ts +151 -0
  226. package/dist/ui/utils/animate.test.ts +324 -0
  227. package/dist/ui/utils/animate.ts +544 -0
  228. package/dist/ui/utils/cn.test.ts +27 -0
  229. package/dist/ui/utils/cn.ts +6 -0
  230. package/dist/utils/analyze.d.ts +32 -0
  231. package/dist/utils/analyze.js +109 -0
  232. package/dist/utils/analyze.js.map +1 -0
  233. package/dist/utils/analyze.test.d.ts +1 -0
  234. package/dist/utils/analyze.test.js +146 -0
  235. package/dist/utils/analyze.test.js.map +1 -0
  236. package/dist/utils/animate.d.ts +210 -0
  237. package/dist/utils/animate.js +423 -0
  238. package/dist/utils/animate.js.map +1 -0
  239. package/dist/utils/animate.test.d.ts +1 -0
  240. package/dist/utils/animate.test.js +365 -0
  241. package/dist/utils/animate.test.js.map +1 -0
  242. package/dist/utils/banner.d.ts +1 -0
  243. package/dist/utils/banner.js +15 -0
  244. package/dist/utils/banner.js.map +1 -0
  245. package/dist/utils/banner.test.d.ts +1 -0
  246. package/dist/utils/banner.test.js +29 -0
  247. package/dist/utils/banner.test.js.map +1 -0
  248. package/dist/utils/cn.d.ts +2 -0
  249. package/dist/utils/cn.js +6 -0
  250. package/dist/utils/cn.js.map +1 -0
  251. package/dist/utils/cn.test.d.ts +1 -0
  252. package/dist/utils/cn.test.js +21 -0
  253. package/dist/utils/cn.test.js.map +1 -0
  254. package/dist/utils/format.d.ts +31 -0
  255. package/dist/utils/format.js +95 -0
  256. package/dist/utils/format.js.map +1 -0
  257. package/dist/utils/format.test.d.ts +1 -0
  258. package/dist/utils/format.test.js +71 -0
  259. package/dist/utils/format.test.js.map +1 -0
  260. package/dist/utils/logger.d.ts +6 -0
  261. package/dist/utils/logger.js +16 -0
  262. package/dist/utils/logger.js.map +1 -0
  263. package/dist/utils/logger.test.d.ts +1 -0
  264. package/dist/utils/logger.test.js +36 -0
  265. package/dist/utils/logger.test.js.map +1 -0
  266. package/dist/utils/resolve-paths.d.ts +12 -0
  267. package/dist/utils/resolve-paths.js +29 -0
  268. package/dist/utils/resolve-paths.js.map +1 -0
  269. package/dist/utils/resolve-paths.test.d.ts +1 -0
  270. package/dist/utils/resolve-paths.test.js +52 -0
  271. package/dist/utils/resolve-paths.test.js.map +1 -0
  272. package/package.json +56 -4
  273. 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,6 @@
1
+ export {
2
+ UiButton,
3
+ buttonVariants,
4
+ type ButtonVariant,
5
+ type ButtonSize,
6
+ } from './button';
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ UiButtonGroup,
3
+ UiButtonGroupItem,
4
+ buttonGroupItemVariants,
5
+ type ButtonGroupVariant,
6
+ type ButtonGroupSize,
7
+ type ButtonGroupOrientation,
8
+ } from './button-group';