@tolle_/tolle-ui 18.2.28 → 18.2.30

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 (279) hide show
  1. package/esm2022/lib/accordion-item.component.mjs +5 -11
  2. package/esm2022/lib/accordion.component.mjs +4 -9
  3. package/esm2022/lib/alert-dialog-dynamic.component.mjs +5 -10
  4. package/esm2022/lib/alert-dialog.component.mjs +44 -59
  5. package/esm2022/lib/alert.component.mjs +5 -11
  6. package/esm2022/lib/aspect-ratio.component.mjs +119 -0
  7. package/esm2022/lib/attachment.component.mjs +334 -0
  8. package/esm2022/lib/avatar-fallback.component.mjs +3 -3
  9. package/esm2022/lib/avatar.component.mjs +4 -9
  10. package/esm2022/lib/badge.component.mjs +4 -9
  11. package/esm2022/lib/breadcrumb-item.component.mjs +3 -3
  12. package/esm2022/lib/breadcrumb-link.component.mjs +3 -3
  13. package/esm2022/lib/breadcrumb-separator.component.mjs +3 -3
  14. package/esm2022/lib/breadcrumb.component.mjs +3 -3
  15. package/esm2022/lib/bubble.component.mjs +301 -0
  16. package/esm2022/lib/button.component.mjs +5 -11
  17. package/esm2022/lib/calendar.component.mjs +5 -11
  18. package/esm2022/lib/card.component.mjs +14 -43
  19. package/esm2022/lib/carousel/carousel.component.mjs +5 -12
  20. package/esm2022/lib/chain-of-thought.component.mjs +380 -0
  21. package/esm2022/lib/chart-pie.component.mjs +386 -0
  22. package/esm2022/lib/chart.component.mjs +1146 -0
  23. package/esm2022/lib/chart.service.mjs +555 -0
  24. package/esm2022/lib/checkbox.component.mjs +5 -11
  25. package/esm2022/lib/checkpoint.component.mjs +134 -0
  26. package/esm2022/lib/collapsible.component.mjs +4 -11
  27. package/esm2022/lib/combobox.component.mjs +372 -0
  28. package/esm2022/lib/command-dialog.component.mjs +164 -0
  29. package/esm2022/lib/command.component.mjs +514 -0
  30. package/esm2022/lib/command.service.mjs +130 -0
  31. package/esm2022/lib/confirmation.component.mjs +219 -0
  32. package/esm2022/lib/context-menu-trigger.directive.mjs +36 -0
  33. package/esm2022/lib/context-menu.component.mjs +460 -0
  34. package/esm2022/lib/context-menu.service.mjs +121 -0
  35. package/esm2022/lib/context.component.mjs +377 -0
  36. package/esm2022/lib/conversation.component.mjs +442 -0
  37. package/esm2022/lib/country-selector.component.mjs +5 -11
  38. package/esm2022/lib/data-table.component.mjs +63 -12
  39. package/esm2022/lib/date-picker.component.mjs +251 -198
  40. package/esm2022/lib/date-range-picker.component.mjs +224 -116
  41. package/esm2022/lib/date-time-picker.component.mjs +506 -0
  42. package/esm2022/lib/direction.component.mjs +79 -0
  43. package/esm2022/lib/dropdown-item.component.mjs +3 -3
  44. package/esm2022/lib/dropdown-label.component.mjs +3 -3
  45. package/esm2022/lib/dropdown-menu.component.mjs +3 -3
  46. package/esm2022/lib/dropdown-separator.component.mjs +3 -3
  47. package/esm2022/lib/dropdown-trigger.directive.mjs +6 -4
  48. package/esm2022/lib/empty-state.component.mjs +4 -9
  49. package/esm2022/lib/field.component.mjs +394 -0
  50. package/esm2022/lib/hover-card.component.mjs +6 -16
  51. package/esm2022/lib/inline-citation.component.mjs +263 -0
  52. package/esm2022/lib/input-group.component.mjs +418 -0
  53. package/esm2022/lib/input.component.mjs +5 -11
  54. package/esm2022/lib/item.component.mjs +243 -0
  55. package/esm2022/lib/kbd.component.mjs +82 -0
  56. package/esm2022/lib/label.component.mjs +4 -9
  57. package/esm2022/lib/marker.component.mjs +135 -0
  58. package/esm2022/lib/masked-input.component.mjs +5 -11
  59. package/esm2022/lib/menubar.component.mjs +683 -0
  60. package/esm2022/lib/message-scroller.component.mjs +353 -0
  61. package/esm2022/lib/message-scroller.service.mjs +157 -0
  62. package/esm2022/lib/message.component.mjs +228 -0
  63. package/esm2022/lib/model-selector.component.mjs +310 -0
  64. package/esm2022/lib/multi-select.component.mjs +155 -72
  65. package/esm2022/lib/native-select.component.mjs +201 -0
  66. package/esm2022/lib/navigation-menu.component.mjs +642 -0
  67. package/esm2022/lib/otp-group.component.mjs +3 -3
  68. package/esm2022/lib/otp-slot.component.mjs +4 -9
  69. package/esm2022/lib/otp.component.mjs +5 -11
  70. package/esm2022/lib/pagination.component.mjs +7 -10
  71. package/esm2022/lib/plan.component.mjs +335 -0
  72. package/esm2022/lib/popover-content.component.mjs +4 -9
  73. package/esm2022/lib/popover.component.mjs +15 -13
  74. package/esm2022/lib/progress.component.mjs +4 -9
  75. package/esm2022/lib/prompt-input.component.mjs +526 -0
  76. package/esm2022/lib/queue.component.mjs +280 -0
  77. package/esm2022/lib/radio-group.component.mjs +6 -13
  78. package/esm2022/lib/radio-item.component.mjs +4 -9
  79. package/esm2022/lib/range-calendar.component.mjs +5 -11
  80. package/esm2022/lib/reasoning.component.mjs +335 -0
  81. package/esm2022/lib/resizable.component.mjs +219 -0
  82. package/esm2022/lib/segment.component.mjs +17 -7
  83. package/esm2022/lib/select-group.component.mjs +4 -8
  84. package/esm2022/lib/select-item.component.mjs +4 -9
  85. package/esm2022/lib/select-separator.component.mjs +4 -8
  86. package/esm2022/lib/select.component.mjs +133 -49
  87. package/esm2022/lib/separator.component.mjs +4 -9
  88. package/esm2022/lib/sheet.component.mjs +38 -56
  89. package/esm2022/lib/shimmer.component.mjs +84 -0
  90. package/esm2022/lib/sidebar.component.mjs +11 -1
  91. package/esm2022/lib/skeleton.component.mjs +4 -9
  92. package/esm2022/lib/sources.component.mjs +452 -0
  93. package/esm2022/lib/spinner.component.mjs +68 -0
  94. package/esm2022/lib/suggestion.component.mjs +207 -0
  95. package/esm2022/lib/switch.component.mjs +5 -11
  96. package/esm2022/lib/table.component.mjs +213 -0
  97. package/esm2022/lib/tabs.component.mjs +4 -11
  98. package/esm2022/lib/tag-input.component.mjs +5 -11
  99. package/esm2022/lib/task.component.mjs +359 -0
  100. package/esm2022/lib/textarea.component.mjs +5 -11
  101. package/esm2022/lib/theme.service.mjs +17 -6
  102. package/esm2022/lib/time-picker.component.mjs +923 -0
  103. package/esm2022/lib/toast.service.mjs +3 -2
  104. package/esm2022/lib/toaster.component.mjs +76 -39
  105. package/esm2022/lib/toggle-group.component.mjs +4 -9
  106. package/esm2022/lib/toggle.component.mjs +5 -11
  107. package/esm2022/lib/tool.component.mjs +462 -0
  108. package/esm2022/lib/typography.component.mjs +83 -0
  109. package/esm2022/lib/utils/color.mjs +140 -19
  110. package/esm2022/public-api.mjs +61 -1
  111. package/fesm2022/tolle-ui.mjs +16457 -1024
  112. package/fesm2022/tolle-ui.mjs.map +1 -1
  113. package/lib/alert-dialog.component.d.ts +10 -2
  114. package/lib/aspect-ratio.component.d.ts +20 -0
  115. package/lib/attachment.component.d.ts +101 -0
  116. package/lib/bubble.component.d.ts +112 -0
  117. package/lib/chain-of-thought.component.d.ts +166 -0
  118. package/lib/chart-pie.component.d.ts +108 -0
  119. package/lib/chart.component.d.ts +369 -0
  120. package/lib/chart.service.d.ts +222 -0
  121. package/lib/checkpoint.component.d.ts +51 -0
  122. package/lib/combobox.component.d.ts +90 -0
  123. package/lib/command-dialog.component.d.ts +53 -0
  124. package/lib/command.component.d.ts +189 -0
  125. package/lib/command.service.d.ts +67 -0
  126. package/lib/confirmation.component.d.ts +70 -0
  127. package/lib/context-menu-trigger.directive.d.ts +12 -0
  128. package/lib/context-menu.component.d.ts +41 -0
  129. package/lib/context-menu.service.d.ts +38 -0
  130. package/lib/context.component.d.ts +134 -0
  131. package/lib/conversation.component.d.ts +165 -0
  132. package/lib/data-table.component.d.ts +27 -1
  133. package/lib/date-picker.component.d.ts +72 -11
  134. package/lib/date-range-picker.component.d.ts +63 -6
  135. package/lib/date-time-picker.component.d.ts +119 -0
  136. package/lib/direction.component.d.ts +45 -0
  137. package/lib/field.component.d.ts +201 -0
  138. package/lib/inline-citation.component.d.ts +103 -0
  139. package/lib/input-group.component.d.ts +150 -0
  140. package/lib/item.component.d.ts +154 -0
  141. package/lib/kbd.component.d.ts +50 -0
  142. package/lib/marker.component.d.ts +58 -0
  143. package/lib/menubar.component.d.ts +247 -0
  144. package/lib/message-scroller.component.d.ts +159 -0
  145. package/lib/message-scroller.service.d.ts +73 -0
  146. package/lib/message.component.d.ts +128 -0
  147. package/lib/model-selector.component.d.ts +103 -0
  148. package/lib/multi-select.component.d.ts +66 -14
  149. package/lib/native-select.component.d.ts +57 -0
  150. package/lib/navigation-menu.component.d.ts +230 -0
  151. package/lib/plan.component.d.ts +120 -0
  152. package/lib/popover.component.d.ts +1 -0
  153. package/lib/prompt-input.component.d.ts +194 -0
  154. package/lib/queue.component.d.ts +106 -0
  155. package/lib/reasoning.component.d.ts +127 -0
  156. package/lib/resizable.component.d.ts +43 -0
  157. package/lib/segment.component.d.ts +13 -3
  158. package/lib/select.component.d.ts +37 -5
  159. package/lib/sheet.component.d.ts +3 -0
  160. package/lib/shimmer.component.d.ts +38 -0
  161. package/lib/sidebar.component.d.ts +2 -0
  162. package/lib/sources.component.d.ts +156 -0
  163. package/lib/spinner.component.d.ts +29 -0
  164. package/lib/suggestion.component.d.ts +96 -0
  165. package/lib/table.component.d.ts +106 -0
  166. package/lib/task.component.d.ts +151 -0
  167. package/lib/theme.service.d.ts +9 -0
  168. package/lib/time-picker.component.d.ts +243 -0
  169. package/lib/toast.service.d.ts +3 -1
  170. package/lib/toaster.component.d.ts +26 -9
  171. package/lib/tool.component.d.ts +158 -0
  172. package/lib/typography.component.d.ts +32 -0
  173. package/lib/utils/color.d.ts +19 -5
  174. package/package.json +1 -1
  175. package/public-api.d.ts +50 -0
  176. package/registry/docs-content.json +8497 -1553
  177. package/registry/llms-full.txt +2679 -318
  178. package/registry/llms.txt +43 -4
  179. package/registry/manifest.json +9093 -1739
  180. package/registry/r/accordion-item.json +1 -1
  181. package/registry/r/accordion.json +1 -1
  182. package/registry/r/alert-dialog-dynamic.json +1 -1
  183. package/registry/r/alert-dialog.json +1 -1
  184. package/registry/r/alert.json +1 -1
  185. package/registry/r/attachment.json +23 -0
  186. package/registry/r/avatar-fallback.json +1 -1
  187. package/registry/r/avatar.json +1 -1
  188. package/registry/r/badge.json +1 -1
  189. package/registry/r/breadcrumb-item.json +1 -1
  190. package/registry/r/breadcrumb-link.json +1 -1
  191. package/registry/r/breadcrumb-separator.json +1 -1
  192. package/registry/r/breadcrumb.json +1 -1
  193. package/registry/r/bubble.json +23 -0
  194. package/registry/r/button.json +1 -1
  195. package/registry/r/calendar.json +1 -1
  196. package/registry/r/card.json +1 -1
  197. package/registry/r/carousel.json +1 -1
  198. package/registry/r/chain-of-thought.json +23 -0
  199. package/registry/r/chart-pie.json +30 -0
  200. package/registry/r/chart.json +28 -0
  201. package/registry/r/checkbox.json +1 -1
  202. package/registry/r/checkpoint.json +23 -0
  203. package/registry/r/collapsible.json +1 -1
  204. package/registry/r/combobox.json +31 -0
  205. package/registry/r/command-dialog.json +21 -0
  206. package/registry/r/command.json +26 -0
  207. package/registry/r/confirmation.json +23 -0
  208. package/registry/r/context.json +26 -0
  209. package/registry/r/conversation.json +23 -0
  210. package/registry/r/country-selector.json +1 -1
  211. package/registry/r/data-table.json +1 -1
  212. package/registry/r/date-picker.json +4 -4
  213. package/registry/r/date-range-picker.json +3 -3
  214. package/registry/r/date-time-picker.json +28 -0
  215. package/registry/r/direction.json +16 -0
  216. package/registry/r/dropdown-item.json +1 -1
  217. package/registry/r/dropdown-label.json +1 -1
  218. package/registry/r/dropdown-menu.json +1 -1
  219. package/registry/r/dropdown-separator.json +1 -1
  220. package/registry/r/dropdown-trigger.json +1 -1
  221. package/registry/r/empty-state.json +1 -1
  222. package/registry/r/field.json +23 -0
  223. package/registry/r/hover-card.json +1 -1
  224. package/registry/r/inline-citation.json +26 -0
  225. package/registry/r/input-group.json +23 -0
  226. package/registry/r/input.json +1 -1
  227. package/registry/r/item.json +23 -0
  228. package/registry/r/kbd.json +23 -0
  229. package/registry/r/label.json +1 -1
  230. package/registry/r/marker.json +23 -0
  231. package/registry/r/masked-input.json +1 -1
  232. package/registry/r/menubar.json +24 -0
  233. package/registry/r/message-scroller.json +26 -0
  234. package/registry/r/message.json +23 -0
  235. package/registry/r/model-selector.json +32 -0
  236. package/registry/r/multi-select.json +2 -2
  237. package/registry/r/native-select.json +23 -0
  238. package/registry/r/navigation-menu.json +24 -0
  239. package/registry/r/otp-group.json +1 -1
  240. package/registry/r/otp-slot.json +1 -1
  241. package/registry/r/otp.json +1 -1
  242. package/registry/r/pagination.json +3 -2
  243. package/registry/r/plan.json +23 -0
  244. package/registry/r/popover-content.json +1 -1
  245. package/registry/r/popover.json +1 -1
  246. package/registry/r/progress.json +1 -1
  247. package/registry/r/prompt-input.json +23 -0
  248. package/registry/r/queue.json +23 -0
  249. package/registry/r/radio-group.json +1 -1
  250. package/registry/r/radio-item.json +1 -1
  251. package/registry/r/range-calendar.json +1 -1
  252. package/registry/r/reasoning.json +23 -0
  253. package/registry/r/segment.json +1 -1
  254. package/registry/r/select-group.json +1 -1
  255. package/registry/r/select-item.json +1 -1
  256. package/registry/r/select-separator.json +1 -1
  257. package/registry/r/select.json +4 -3
  258. package/registry/r/separator.json +1 -1
  259. package/registry/r/sheet.json +1 -1
  260. package/registry/r/shimmer.json +23 -0
  261. package/registry/r/sidebar.json +1 -1
  262. package/registry/r/skeleton.json +1 -1
  263. package/registry/r/sources.json +23 -0
  264. package/registry/r/spinner.json +23 -0
  265. package/registry/r/suggestion.json +23 -0
  266. package/registry/r/switch.json +1 -1
  267. package/registry/r/table.json +21 -0
  268. package/registry/r/tabs.json +1 -1
  269. package/registry/r/tag-input.json +1 -1
  270. package/registry/r/task.json +23 -0
  271. package/registry/r/textarea.json +1 -1
  272. package/registry/r/time-picker.json +24 -0
  273. package/registry/r/toaster.json +2 -2
  274. package/registry/r/toggle-group.json +1 -1
  275. package/registry/r/toggle.json +1 -1
  276. package/registry/r/tool.json +23 -0
  277. package/registry/r/typography.json +23 -0
  278. package/registry/registry.json +915 -22
  279. package/theme.css +109 -8
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "accordion-item.component.ts",
12
12
  "target": "ui/accordion-item.component.ts",
13
- "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, state, style, transition, animate } from '@angular/animations';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion-item',\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('expandCollapse', [\n state('collapsed', style({\n height: '0px',\n opacity: '0',\n overflow: 'hidden',\n visibility: 'hidden'\n })),\n state('expanded', style({\n height: '*', // \"Star\" means actual content height\n opacity: '1',\n overflow: 'hidden',\n visibility: 'visible'\n })),\n // Use cubic-bezier to match Tailwind/shadcn-ui default ease\n transition('collapsed <=> expanded', [\n animate('300ms cubic-bezier(0.87, 0, 0.13, 1)')\n ])\n ])\n ],\n template: `\n <div [class]=\"cn('flex flex-col border-b border-border', class)\">\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [id]=\"'acc-trigger-'+id\"\n [attr.aria-controls]=\"'acc-content-'+id\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"flex flex-1 items-center justify-between py-4 font-medium transition-all group [&[data-state=open]>i]:rotate-180 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background\"\n >\n <span class=\"text-left group-hover:underline\">{{ title }}</span>\n <i class=\"ri-arrow-down-s-line text-muted-foreground text-lg transition-transform duration-200\"></i>\n </button>\n\n <div\n [@expandCollapse]=\"isOpen ? 'expanded' : 'collapsed'\"\n [id]=\"'acc-content-'+id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-trigger-'+id\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"overflow-hidden\">\n <div class=\"pb-4 pt-0 text-sm text-muted-foreground\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AccordionItemComponent {\n @Input() title: string = '';\n @Input() class: string = '';\n @Input() id!: string | number;\n\n isOpen = false;\n\n // This will be set by the parent Accordion component\n onToggle?: (id: string | number) => void;\n\n toggle() {\n if (this.onToggle) {\n this.onToggle(this.id);\n } else {\n this.isOpen = !this.isOpen;\n }\n }\n\n protected readonly cn = cn;\n}\n"
13
+ "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, state, style, transition, animate } from '@angular/animations';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion-item',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('expandCollapse', [\n state('collapsed', style({\n height: '0px',\n opacity: '0',\n overflow: 'hidden',\n visibility: 'hidden'\n })),\n state('expanded', style({\n height: '*', // \"Star\" means actual content height\n opacity: '1',\n overflow: 'hidden',\n visibility: 'visible'\n })),\n // Use cubic-bezier to match Tailwind/shadcn-ui default ease\n transition('collapsed <=> expanded', [\n animate('300ms cubic-bezier(0.87, 0, 0.13, 1)')\n ])\n ])\n ],\n template: `\n <div [class]=\"cn('flex flex-col border-b border-border', class)\">\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [id]=\"'acc-trigger-'+id\"\n [attr.aria-controls]=\"'acc-content-'+id\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"flex flex-1 items-center justify-between py-4 font-medium transition-all group [&[data-state=open]>i]:rotate-180 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background\"\n >\n <span class=\"text-left group-hover:underline\">{{ title }}</span>\n <i class=\"ri-arrow-down-s-line text-muted-foreground text-lg transition-transform duration-200\"></i>\n </button>\n\n <div\n [@expandCollapse]=\"isOpen ? 'expanded' : 'collapsed'\"\n [id]=\"'acc-content-'+id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-trigger-'+id\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n class=\"overflow-hidden\">\n <div class=\"pb-4 pt-0 text-sm text-muted-foreground\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AccordionItemComponent {\n @Input() title: string = '';\n @Input() class: string = '';\n @Input() id!: string | number;\n\n isOpen = false;\n\n // This will be set by the parent Accordion component\n onToggle?: (id: string | number) => void;\n\n toggle() {\n if (this.onToggle) {\n this.onToggle(this.id);\n } else {\n this.isOpen = !this.isOpen;\n }\n }\n\n protected readonly cn = cn;\n}\n"
14
14
  },
15
15
  {
16
16
  "path": "utils/cn.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "accordion.component.ts",
14
14
  "target": "ui/accordion.component.ts",
15
- "content": "import { Component, Input, ContentChildren, QueryList, AfterContentInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AccordionItemComponent } from './accordion-item.component';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion',\n standalone: true,\n imports: [CommonModule], // No AccordionItemComponent import needed here if projected via ng-content\n template: `\n <div [class]=\"cn('w-full', class)\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AccordionComponent implements AfterContentInit {\n @Input() type: 'single' | 'multiple' = 'single';\n @Input() class: string = '';\n\n @ContentChildren(AccordionItemComponent) items!: QueryList<AccordionItemComponent>;\n\n ngAfterContentInit() {\n // 1. Assign IDs and Listeners on load\n this.initItems();\n\n // 2. Re-init if items change dynamically (optional but good for robustness)\n this.items.changes.subscribe(() => this.initItems());\n }\n\n private initItems() {\n this.items.forEach((item, index) => {\n // Auto-assign ID if missing\n if (item.id === undefined) item.id = `accordion-item-${index}`;\n\n // Set up the toggle bridge\n item.onToggle = (id) => this.handleToggle(id);\n });\n }\n\n private handleToggle(selectedId: string | number) {\n this.items.forEach(item => {\n if (item.id === selectedId) {\n // Toggle the clicked item\n item.isOpen = !item.isOpen;\n } else if (this.type === 'single') {\n // Close others if in single mode\n item.isOpen = false;\n }\n });\n }\n\n protected readonly cn = cn;\n}\n"
15
+ "content": "import { Component, Input, ContentChildren, QueryList, AfterContentInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AccordionItemComponent } from './accordion-item.component';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-accordion',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule], // No AccordionItemComponent import needed here if projected via ng-content\n template: `\n <div [class]=\"cn('w-full', class)\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AccordionComponent implements AfterContentInit {\n @Input() type: 'single' | 'multiple' = 'single';\n @Input() class: string = '';\n\n @ContentChildren(AccordionItemComponent) items!: QueryList<AccordionItemComponent>;\n\n ngAfterContentInit() {\n // 1. Assign IDs and Listeners on load\n this.initItems();\n\n // 2. Re-init if items change dynamically (optional but good for robustness)\n this.items.changes.subscribe(() => this.initItems());\n }\n\n private initItems() {\n this.items.forEach((item, index) => {\n // Auto-assign ID if missing\n if (item.id === undefined) item.id = `accordion-item-${index}`;\n\n // Set up the toggle bridge\n item.onToggle = (id) => this.handleToggle(id);\n });\n }\n\n private handleToggle(selectedId: string | number) {\n this.items.forEach(item => {\n if (item.id === selectedId) {\n // Toggle the clicked item\n item.isOpen = !item.isOpen;\n } else if (this.type === 'single') {\n // Close others if in single mode\n item.isOpen = false;\n }\n });\n }\n\n protected readonly cn = cn;\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "alert-dialog-dynamic.component.ts",
17
17
  "target": "ui/alert-dialog-dynamic.component.ts",
18
- "content": "import { Component, Inject, inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent\n} from './alert-dialog.component';\nimport { ButtonComponent } from './button.component';\nimport { AlertDialogConfig, AlertDialogRef } from './alert-dialog.types';\n\n@Component({\n selector: 'tolle-alert-dialog-dynamic',\n standalone: true,\n imports: [\n CommonModule,\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent,\n ButtonComponent\n ],\n template: `\n <tolle-alert-dialog [open]=\"true\" (openChange)=\"onOpenChange($event)\">\n <tolle-alert-dialog-content [size]=\"config.size || 'lg'\">\n <tolle-alert-dialog-header>\n <tolle-alert-dialog-title>{{ config.title }}</tolle-alert-dialog-title>\n <tolle-alert-dialog-description>\n {{ config.description }}\n </tolle-alert-dialog-description>\n </tolle-alert-dialog-header>\n <tolle-alert-dialog-footer>\n <tolle-alert-dialog-cancel (click)=\"close(false)\">\n <tolle-button variant=\"outline\">{{ config.cancelText || 'Cancel' }}</tolle-button>\n </tolle-alert-dialog-cancel>\n <tolle-alert-dialog-action (click)=\"close(true)\">\n <tolle-button [variant]=\"config.variant === 'destructive' ? 'destructive' : 'default'\">\n {{ config.actionText || 'Continue' }}\n </tolle-button>\n </tolle-alert-dialog-action>\n </tolle-alert-dialog-footer>\n </tolle-alert-dialog-content>\n </tolle-alert-dialog>\n `\n})\nexport class AlertDialogDynamicComponent {\n config!: AlertDialogConfig;\n dialogRef!: AlertDialogRef;\n\n onOpenChange(open: boolean) {\n if (!open) {\n this.close(false);\n }\n }\n\n close(result: boolean) {\n this.dialogRef.close(result);\n }\n}\n"
18
+ "content": "import { Component, Inject, inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent\n} from './alert-dialog.component';\nimport { ButtonComponent } from './button.component';\nimport { AlertDialogConfig, AlertDialogRef } from './alert-dialog.types';\n\n@Component({\n selector: 'tolle-alert-dialog-dynamic',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [\n CommonModule,\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent,\n ButtonComponent\n ],\n template: `\n <tolle-alert-dialog [open]=\"true\" (openChange)=\"onOpenChange($event)\">\n <tolle-alert-dialog-content [size]=\"config.size || 'lg'\">\n <tolle-alert-dialog-header>\n <tolle-alert-dialog-title>{{ config.title }}</tolle-alert-dialog-title>\n <tolle-alert-dialog-description>\n {{ config.description }}\n </tolle-alert-dialog-description>\n </tolle-alert-dialog-header>\n <tolle-alert-dialog-footer>\n <tolle-alert-dialog-cancel (click)=\"close(false)\">\n <tolle-button variant=\"outline\">{{ config.cancelText || 'Cancel' }}</tolle-button>\n </tolle-alert-dialog-cancel>\n <tolle-alert-dialog-action (click)=\"close(true)\">\n <tolle-button [variant]=\"config.variant === 'destructive' ? 'destructive' : 'default'\">\n {{ config.actionText || 'Continue' }}\n </tolle-button>\n </tolle-alert-dialog-action>\n </tolle-alert-dialog-footer>\n </tolle-alert-dialog-content>\n </tolle-alert-dialog>\n `\n})\nexport class AlertDialogDynamicComponent {\n config!: AlertDialogConfig;\n dialogRef!: AlertDialogRef;\n\n onOpenChange(open: boolean) {\n if (!open) {\n this.close(false);\n }\n }\n\n close(result: boolean) {\n this.dialogRef.close(result);\n }\n}\n"
19
19
  },
20
20
  {
21
21
  "path": "alert-dialog.types.ts",
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "alert-dialog.component.ts",
12
12
  "target": "ui/alert-dialog.component.ts",
13
- "content": "import { Component, Input, Output, EventEmitter, Injectable, inject, TemplateRef, ViewChild, ViewContainerRef, OnDestroy, OnInit, ContentChild, forwardRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';\nimport { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { cn } from './utils/cn';\nimport { BehaviorSubject } from 'rxjs';\nimport { AlertDialogSize } from './alert-dialog.types';\n\n@Injectable()\nclass AlertDialogInternalService {\n private openSubject = new BehaviorSubject<boolean>(false);\n open$ = this.openSubject.asObservable();\n\n setOpen(value: boolean) {\n this.openSubject.next(value);\n }\n\n getOpen() {\n return this.openSubject.getValue();\n }\n\n toggle() {\n this.setOpen(!this.getOpen());\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog',\n standalone: true,\n imports: [CommonModule],\n providers: [AlertDialogInternalService],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogComponent {\n @Input() set open(val: boolean) {\n this.alertDialogService.setOpen(val);\n }\n @Output() openChange = new EventEmitter<boolean>();\n\n private alertDialogService = inject(AlertDialogInternalService);\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.openChange.emit(val);\n });\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-trigger',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n '(click)': 'onOpen()'\n }\n})\nexport class AlertDialogTriggerComponent {\n private alertDialogService = inject(AlertDialogInternalService);\n\n onOpen() {\n this.alertDialogService.setOpen(true);\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-portal',\n standalone: true,\n imports: [CommonModule],\n template: `\n <ng-template #portalContent>\n <ng-content></ng-content>\n </ng-template>\n `\n})\nexport class AlertDialogPortalComponent implements OnInit, OnDestroy {\n @ViewChild('portalContent', { static: true }) portalContent!: TemplateRef<any>;\n private alertDialogService = inject(AlertDialogInternalService);\n private overlay = inject(Overlay);\n private viewContainerRef = inject(ViewContainerRef);\n private overlayRef?: OverlayRef;\n /** Element focused before the dialog opened, restored on close. */\n private previouslyFocused?: HTMLElement | null;\n\n ngOnInit() {\n this.alertDialogService.open$.subscribe(open => {\n if (open) {\n this.show();\n } else {\n this.hide();\n }\n });\n }\n\n private show() {\n if (this.overlayRef) return;\n\n // Remember the trigger so focus can be restored on close.\n this.previouslyFocused = document.activeElement as HTMLElement | null;\n\n const config = new OverlayConfig({\n hasBackdrop: true,\n backdropClass: ['cdk-overlay-backdrop', 'bg-black/80', 'backdrop-blur-sm', 'data-[state=open]:animate-in', 'data-[state=closed]:animate-out', 'data-[state=closed]:fade-out-0', 'data-[state=open]:fade-in-0'],\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\n scrollStrategy: this.overlay.scrollStrategies.block()\n });\n\n this.overlayRef = this.overlay.create(config);\n\n // Alert-dialog semantics: backdrop clicks must NOT dismiss.\n // Escape still closes the dialog.\n this.overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.alertDialogService.setOpen(false);\n }\n });\n\n const portal = new TemplatePortal(this.portalContent, this.viewContainerRef);\n this.overlayRef.attach(portal);\n }\n\n private hide() {\n if (this.overlayRef) {\n this.overlayRef.detach();\n this.overlayRef.dispose();\n this.overlayRef = undefined;\n // Restore focus to the trigger.\n this.previouslyFocused?.focus?.();\n }\n }\n\n ngOnDestroy() {\n this.hide();\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-content',\n standalone: true,\n imports: [CommonModule, A11yModule],\n template: `<div [class]=\"computedClass\" [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleCmp?.id || null\"\n [attr.aria-describedby]=\"descriptionCmp?.id || null\"\n cdkTrapFocus cdkTrapFocusAutoCapture><ng-content></ng-content></div>`,\n host: {\n '[class]': '\"contents\"'\n }\n})\nexport class AlertDialogContentComponent {\n @Input() class: string = '';\n @Input() size: AlertDialogSize = 'lg';\n\n /** Projected title/description used to build the dialog's accessible name. */\n @ContentChild(forwardRef(() => AlertDialogTitleComponent)) titleCmp?: AlertDialogTitleComponent;\n @ContentChild(forwardRef(() => AlertDialogDescriptionComponent)) descriptionCmp?: AlertDialogDescriptionComponent;\n\n private alertDialogService = inject(AlertDialogInternalService);\n isOpen = false;\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.isOpen = val;\n });\n }\n\n get computedClass() {\n return cn(\n \"fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-50 grid w-full gap-4 border border-input bg-background p-6 shadow-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] rounded-lg\",\n {\n 'max-w-xs': this.size === 'xs',\n 'max-w-sm': this.size === 'sm',\n 'max-w-md': this.size === 'md',\n 'max-w-lg': this.size === 'lg',\n 'max-w-xl': this.size === 'xl',\n 'max-w-2xl': this.size === '2xl',\n 'max-w-full': this.size === 'full',\n 'max-w-fit': this.size === 'fit',\n },\n this.class\n );\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-header',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogHeaderComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-col space-y-2 text-center sm:text-left\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-footer',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogFooterComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-row justify-end space-x-2\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-title',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogTitleComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-labelledby`. */\n readonly id = `tolle-alert-dialog-title-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-lg font-semibold text-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-description',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogDescriptionComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-describedby`. */\n readonly id = `tolle-alert-dialog-description-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-sm text-muted-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-action',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogActionComponent {\n}\n\n@Component({\n selector: 'tolle-alert-dialog-cancel',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogCancelComponent {\n}\n"
13
+ "content": "import { Component, Input, Output, EventEmitter, HostListener, Injectable, inject, TemplateRef, ViewChild, ViewContainerRef, OnDestroy, OnInit, ContentChild, forwardRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';\nimport { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { cn } from './utils/cn';\nimport { BehaviorSubject } from 'rxjs';\nimport { AlertDialogSize } from './alert-dialog.types';\n\n@Injectable()\nclass AlertDialogInternalService {\n private openSubject = new BehaviorSubject<boolean>(false);\n open$ = this.openSubject.asObservable();\n\n setOpen(value: boolean) {\n this.openSubject.next(value);\n }\n\n getOpen() {\n return this.openSubject.getValue();\n }\n\n toggle() {\n this.setOpen(!this.getOpen());\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n providers: [AlertDialogInternalService],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogComponent {\n @Input() set open(val: boolean) {\n this.alertDialogService.setOpen(val);\n }\n @Output() openChange = new EventEmitter<boolean>();\n\n private alertDialogService = inject(AlertDialogInternalService);\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.openChange.emit(val);\n });\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-trigger',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n '(click)': 'onOpen()'\n }\n})\nexport class AlertDialogTriggerComponent {\n private alertDialogService = inject(AlertDialogInternalService);\n\n onOpen() {\n this.alertDialogService.setOpen(true);\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-portal',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <ng-template #portalContent>\n <ng-content></ng-content>\n </ng-template>\n `\n})\nexport class AlertDialogPortalComponent implements OnInit, OnDestroy {\n @ViewChild('portalContent', { static: true }) portalContent!: TemplateRef<any>;\n private alertDialogService = inject(AlertDialogInternalService);\n private overlay = inject(Overlay);\n private viewContainerRef = inject(ViewContainerRef);\n private overlayRef?: OverlayRef;\n /** Element focused before the dialog opened, restored on close. */\n private previouslyFocused?: HTMLElement | null;\n\n ngOnInit() {\n this.alertDialogService.open$.subscribe(open => {\n if (open) {\n this.show();\n } else {\n this.hide();\n }\n });\n }\n\n private show() {\n if (this.overlayRef) return;\n\n // Remember the trigger so focus can be restored on close.\n this.previouslyFocused = document.activeElement as HTMLElement | null;\n\n const config = new OverlayConfig({\n hasBackdrop: true,\n backdropClass: ['cdk-overlay-backdrop', 'bg-black/80', 'backdrop-blur-sm', 'data-[state=open]:animate-in', 'data-[state=closed]:animate-out', 'data-[state=closed]:fade-out-0', 'data-[state=open]:fade-in-0'],\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\n scrollStrategy: this.overlay.scrollStrategies.block()\n });\n\n this.overlayRef = this.overlay.create(config);\n\n // Alert-dialog semantics: backdrop clicks must NOT dismiss.\n // Escape still closes the dialog.\n this.overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n this.alertDialogService.setOpen(false);\n }\n });\n\n const portal = new TemplatePortal(this.portalContent, this.viewContainerRef);\n this.overlayRef.attach(portal);\n }\n\n private hide() {\n if (this.overlayRef) {\n this.overlayRef.detach();\n this.overlayRef.dispose();\n this.overlayRef = undefined;\n // Restore focus to the trigger.\n this.previouslyFocused?.focus?.();\n }\n }\n\n ngOnDestroy() {\n this.hide();\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-content',\n standalone: true,\n imports: [CommonModule, A11yModule],\n template: `<div [class]=\"computedClass\" [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleCmp?.id || null\"\n [attr.aria-describedby]=\"descriptionCmp?.id || null\"\n cdkTrapFocus cdkTrapFocusAutoCapture><ng-content></ng-content></div>`,\n host: {\n '[class]': '\"contents\"'\n }\n})\nexport class AlertDialogContentComponent {\n @Input() class: string = '';\n @Input() size: AlertDialogSize = 'lg';\n\n /** Projected title/description used to build the dialog's accessible name. */\n @ContentChild(forwardRef(() => AlertDialogTitleComponent)) titleCmp?: AlertDialogTitleComponent;\n @ContentChild(forwardRef(() => AlertDialogDescriptionComponent)) descriptionCmp?: AlertDialogDescriptionComponent;\n\n private alertDialogService = inject(AlertDialogInternalService);\n isOpen = false;\n\n constructor() {\n this.alertDialogService.open$.subscribe(val => {\n this.isOpen = val;\n });\n }\n\n get computedClass() {\n return cn(\n \"fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-50 grid w-full gap-4 border border-input bg-background p-6 shadow-lg data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] rounded-lg\",\n {\n 'max-w-xs': this.size === 'xs',\n 'max-w-sm': this.size === 'sm',\n 'max-w-md': this.size === 'md',\n 'max-w-lg': this.size === 'lg',\n 'max-w-xl': this.size === 'xl',\n 'max-w-2xl': this.size === '2xl',\n 'max-w-full': this.size === 'full',\n 'max-w-fit': this.size === 'fit',\n },\n this.class\n );\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-header',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogHeaderComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-col space-y-2 text-center sm:text-left\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-footer',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass' }\n})\nexport class AlertDialogFooterComponent {\n @Input() class: string = '';\n get computedClass() { return cn(\"flex flex-row justify-end space-x-2\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-title',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogTitleComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-labelledby`. */\n readonly id = `tolle-alert-dialog-title-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-lg font-semibold text-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-description',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: { '[class]': 'computedClass', '[attr.id]': 'id' }\n})\nexport class AlertDialogDescriptionComponent {\n @Input() class: string = '';\n /** Stable id referenced by the dialog's `aria-describedby`. */\n readonly id = `tolle-alert-dialog-description-${Math.random().toString(36).substr(2, 9)}`;\n get computedClass() { return cn(\"text-sm text-muted-foreground\", this.class); }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-action',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogActionComponent {\n private alertDialogService = inject(AlertDialogInternalService, { optional: true });\n\n /** Fires before the dialog closes, so consumers can run the confirmed action. */\n @Output() confirmed = new EventEmitter<MouseEvent>();\n\n @HostListener('click', ['$event'])\n handleClick(event: MouseEvent) {\n this.confirmed.emit(event);\n this.alertDialogService?.setOpen(false);\n }\n}\n\n@Component({\n selector: 'tolle-alert-dialog-cancel',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class AlertDialogCancelComponent {\n private alertDialogService = inject(AlertDialogInternalService, { optional: true });\n\n /** Fires before the dialog closes, so consumers can react to a dismissal. */\n @Output() cancelled = new EventEmitter<MouseEvent>();\n\n @HostListener('click', ['$event'])\n handleClick(event: MouseEvent) {\n this.cancelled.emit(event);\n this.alertDialogService?.setOpen(false);\n }\n}\n"
14
14
  },
15
15
  {
16
16
  "path": "alert-dialog.types.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "alert.component.ts",
14
14
  "target": "ui/alert.component.ts",
15
- "content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, transition, style, animate } from '@angular/animations';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border p-4 [&>i+div]:translate-y-[-3px] [&>i]:absolute [&>i]:left-4 [&>i]:top-4 [&>i]:text-foreground [&>i~div]:pl-7\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n destructive: \"border-destructive/50 text-destructive [&>i]:text-destructive\",\n success: \"border-success/50 text-success [&>i]:text-success\",\n warning: \"border-warning/50 text-warning [&>i]:text-warning\",\n info: \"border-info/50 text-info [&>i]:text-info\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\ntype AlertVariants = VariantProps<typeof alertVariants>;\n\n@Component({\n selector: 'tolle-alert',\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('fade', [\n transition(':leave', [\n style({ opacity: 1, transform: 'scale(1)' }),\n animate('300ms ease-in-out', style({ opacity: 0, transform: 'scale(0.95)', height: 0, margin: 0, padding: 0 }))\n ])\n ])\n ],\n template: `\n <div\n *ngIf=\"!dismissed\"\n @fade\n [class]=\"cn(alertVariants({ variant }), class)\"\n role=\"alert\"\n >\n <ng-content select=\"[icon]\"></ng-content>\n\n <button\n *ngIf=\"dismissible\"\n (click)=\"dismiss()\"\n class=\"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n type=\"button\"\n >\n <i class=\"ri-close-line text-lg\"></i>\n <span class=\"sr-only\">Close</span>\n </button>\n\n <div>\n <h5 *ngIf=\"title\" class=\"mb-1 font-medium leading-none tracking-tight\">\n {{ title }}\n </h5>\n <div class=\"text-sm [&_p]:leading-relaxed opacity-90\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AlertComponent {\n @Input() variant: AlertVariants['variant'] = 'default';\n @Input() title?: string;\n @Input() class: string = '';\n @Input() dismissible: boolean = false;\n\n @Output() onClose = new EventEmitter<void>();\n\n dismissed = false;\n protected alertVariants = alertVariants;\n protected cn = cn;\n\n dismiss() {\n this.dismissed = true;\n this.onClose.emit();\n }\n}\n"
15
+ "content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { trigger, transition, style, animate } from '@angular/animations';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border p-4 [&>i+div]:translate-y-[-3px] [&>i]:absolute [&>i]:left-4 [&>i]:top-4 [&>i]:text-foreground [&>i~div]:pl-7\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n destructive: \"border-destructive/50 text-destructive [&>i]:text-destructive\",\n success: \"border-success/50 text-success [&>i]:text-success\",\n warning: \"border-warning/50 text-warning [&>i]:text-warning\",\n info: \"border-info/50 text-info [&>i]:text-info\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\ntype AlertVariants = VariantProps<typeof alertVariants>;\n\n@Component({\n selector: 'tolle-alert',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n animations: [\n trigger('fade', [\n transition(':leave', [\n style({ opacity: 1, transform: 'scale(1)' }),\n animate('300ms ease-in-out', style({ opacity: 0, transform: 'scale(0.95)', height: 0, margin: 0, padding: 0 }))\n ])\n ])\n ],\n template: `\n <div\n *ngIf=\"!dismissed\"\n @fade\n [class]=\"cn(alertVariants({ variant }), class)\"\n role=\"alert\"\n >\n <ng-content select=\"[icon]\"></ng-content>\n\n <button\n *ngIf=\"dismissible\"\n (click)=\"dismiss()\"\n class=\"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n type=\"button\"\n >\n <i class=\"ri-close-line text-lg\"></i>\n <span class=\"sr-only\">Close</span>\n </button>\n\n <div>\n <h5 *ngIf=\"title\" class=\"mb-1 font-medium leading-none tracking-tight\">\n {{ title }}\n </h5>\n <div class=\"text-sm [&_p]:leading-relaxed opacity-90\">\n <ng-content></ng-content>\n </div>\n </div>\n </div>\n `\n})\nexport class AlertComponent {\n @Input() variant: AlertVariants['variant'] = 'default';\n @Input() title?: string;\n @Input() class: string = '';\n @Input() dismissible: boolean = false;\n\n @Output() onClose = new EventEmitter<void>();\n\n dismissed = false;\n protected alertVariants = alertVariants;\n protected cn = cn;\n\n dismiss() {\n this.dismissed = true;\n this.onClose.emit();\n }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "attachment",
3
+ "type": "registry:ui",
4
+ "title": "Attachment",
5
+ "description": "Attachment component with state and size options.",
6
+ "category": "ai",
7
+ "registryDependencies": [],
8
+ "dependencies": [
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "attachment.component.ts",
14
+ "target": "ui/attachment.component.ts",
15
+ "content": "import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst attachmentVariants = cva(\n 'group/attachment relative flex w-full max-w-xs items-center gap-3 overflow-hidden rounded-lg border text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n {\n variants: {\n state: {\n idle: 'border-border bg-card text-card-foreground hover:bg-accent',\n uploading: 'border-border bg-card text-card-foreground',\n error: 'border-destructive bg-destructive/10 text-foreground',\n },\n size: {\n sm: 'gap-2 p-1.5 text-xs',\n default: 'gap-3 p-2 text-sm',\n },\n },\n defaultVariants: { state: 'idle', size: 'default' },\n }\n);\n\nexport type AttachmentProps = VariantProps<typeof attachmentVariants>;\n\n/**\n * A file or image attached to a chat message, with an upload state and an\n * optional progress bar. Wrap several in `tolle-attachment-group`.\n * @new\n */\n@Component({\n selector: 'tolle-attachment',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div\n [class]=\"computedClass\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"name\"\n (click)=\"onOpen($event)\"\n (keydown.enter)=\"onOpen($event)\"\n (keydown.space)=\"onOpen($event)\">\n <img\n *ngIf=\"isImage; else fileIcon\"\n [src]=\"url\"\n [alt]=\"name\"\n class=\"size-10 shrink-0 rounded-md object-cover\" />\n\n <ng-template #fileIcon>\n <span class=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n <i [class]=\"iconClass\" aria-hidden=\"true\"></i>\n </span>\n </ng-template>\n\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span class=\"truncate font-medium leading-none\">{{ name }}</span>\n\n <span class=\"truncate text-xs text-muted-foreground\">\n <ng-container *ngIf=\"state === 'error'; else metaText\">{{ errorLabel }}</ng-container>\n <ng-template #metaText>{{ formattedSize }}</ng-template>\n </span>\n\n <span\n *ngIf=\"state === 'uploading'\"\n class=\"mt-1 block h-1 w-full overflow-hidden rounded-full bg-muted\"\n role=\"progressbar\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-valuenow]=\"clampedProgress\">\n <span\n class=\"block h-full rounded-full bg-primary transition-all\"\n [style.width.%]=\"clampedProgress\"></span>\n </span>\n </span>\n\n <button\n *ngIf=\"removable\"\n type=\"button\"\n [attr.aria-label]=\"removeLabel\"\n (click)=\"onRemove($event)\"\n class=\"shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <i class=\"ri-close-line\" aria-hidden=\"true\"></i>\n </button>\n\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class AttachmentComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** File name shown as the card's title. */\n @Input() name = '';\n /** File size in bytes; rendered as B / KB / MB. @default 0 */\n @Input() size = 0;\n /** MIME type, e.g. 'image/png'. Chooses the thumbnail or the file icon. */\n @Input() type = '';\n /** Source for the thumbnail and the target of `open`. */\n @Input() url = '';\n /** Upload lifecycle state of the attachment. @default 'idle' */\n @Input() state: AttachmentProps['state'] = 'idle';\n /** Upload progress from 0 to 100; only shown while uploading. @default 0 */\n @Input() progress = 0;\n /** Density of the card. @default 'default' */\n @Input() density: AttachmentProps['size'] = 'default';\n /** Shows a trailing remove button that emits `remove`. @default false */\n @Input() removable = false;\n /** Accessible label for the remove button. @default 'Remove attachment' */\n @Input() removeLabel = 'Remove attachment';\n /** Text shown in place of the size when the state is 'error'. @default 'Upload failed' */\n @Input() errorLabel = 'Upload failed';\n /** Extra Tailwind classes merged onto the card via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted when the remove button is pressed. */\n @Output() remove = new EventEmitter<Event>();\n /** Emitted when the card is activated by click, Enter, or Space. */\n @Output() open = new EventEmitter<Event>();\n\n get computedClass(): string {\n return cn(attachmentVariants({ state: this.state, size: this.density }), this.class);\n }\n\n /** Whether the attachment renders as an image thumbnail rather than a file icon. */\n get isImage(): boolean {\n return this.type.startsWith('image/') && !!this.url;\n }\n\n /** Remixicon class picked from the MIME type. */\n get iconClass(): string {\n return this.iconForType(this.type);\n }\n\n /** The `size` input rendered for display, e.g. 1536 becomes \"1.5 KB\". */\n get formattedSize(): string {\n return this.formatBytes(this.size);\n }\n\n /** `progress` constrained to the 0-100 the progress bar can render. */\n get clampedProgress(): number {\n if (!Number.isFinite(this.progress)) return 0;\n return Math.min(100, Math.max(0, this.progress));\n }\n\n private formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return bytes + ' B';\n\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n\n // One decimal, with a trailing \".0\" dropped: 1536 -> \"1.5 KB\", 2048 -> \"2 KB\".\n return Math.round(value * 10) / 10 + ' ' + units[unit];\n }\n\n private iconForType(type: string): string {\n if (type.startsWith('image/')) return 'ri-image-line';\n if (type.startsWith('video/')) return 'ri-film-line';\n if (type.startsWith('audio/')) return 'ri-music-2-line';\n if (type === 'application/pdf') return 'ri-file-pdf-line';\n if (type.startsWith('text/')) return 'ri-file-text-line';\n if (type.includes('zip') || type.includes('compressed') || type.includes('tar')) {\n return 'ri-file-zip-line';\n }\n if (type.includes('spreadsheet') || type.includes('excel') || type.includes('csv')) {\n return 'ri-file-excel-line';\n }\n return 'ri-file-line';\n }\n\n protected onRemove(event: Event): void {\n // The card itself is a trigger; a remove click must not also open it.\n event.stopPropagation();\n this.remove.emit(event);\n }\n\n protected onOpen(event: Event): void {\n if (event.type === 'keydown') event.preventDefault();\n this.open.emit(event);\n }\n}\n\n/** Wrapping row of attachment cards below or above a message. */\n@Component({\n selector: 'tolle-attachment-group',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex flex-wrap items-start gap-2', class)\"><ng-content></ng-content></div>`,\n})\nexport class AttachmentGroupComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the row via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/**\n * Trailing slot for attachment controls — download, retry, remove. Clicks are\n * kept inside the slot so the card's own trigger does not also fire.\n */\n@Component({\n selector: 'tolle-attachment-actions',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"computedClass\" (click)=\"onClick($event)\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class AttachmentActionsComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Keeps the actions visible instead of revealing them on hover or focus. @default false */\n @Input() alwaysVisible = false;\n /** Extra Tailwind classes merged onto the slot via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass(): string {\n return cn(\n 'flex shrink-0 items-center gap-1 transition-opacity',\n this.alwaysVisible\n ? 'opacity-100'\n : 'opacity-0 focus-within:opacity-100 group-hover/attachment:opacity-100',\n this.class\n );\n }\n\n protected onClick(event: Event): void {\n event.stopPropagation();\n }\n}\n"
16
+ },
17
+ {
18
+ "path": "utils/cn.ts",
19
+ "target": "ui/utils/cn.ts",
20
+ "content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
21
+ }
22
+ ]
23
+ }
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "avatar-fallback.component.ts",
12
12
  "target": "ui/avatar-fallback.component.ts",
13
- "content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-avatar-fallback',\n standalone: true,\n imports: [],\n template: `\n <div class=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-medium uppercase\">\n <ng-content></ng-content>\n </div>\n `,\n styles: ``\n})\nexport class AvatarFallbackComponent {\n\n}\n"
13
+ "content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-avatar-fallback',\n standalone: true,\n imports: [],\n template: `\n <div class=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-medium uppercase\">\n <ng-content></ng-content>\n </div>\n `,\n styles: `:host { display: inline-flex; }`\n})\nexport class AvatarFallbackComponent {\n\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "avatar.component.ts",
14
14
  "target": "ui/avatar.component.ts",
15
- "content": "import { ChangeDetectorRef, Component, Input, HostBinding } from '@angular/core';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\nimport { NgIf } from '@angular/common';\n\nconst avatarVariants = cva(\n 'relative flex shrink-0 overflow-hidden bg-muted',\n {\n variants: {\n shape: {\n circle: 'rounded-full',\n square: 'rounded-md',\n },\n size: {\n sm: 'h-8 w-8 text-xs',\n default: 'h-10 w-10',\n lg: 'h-16 w-16 text-lg',\n xl: 'h-24 w-24 text-xl',\n },\n },\n defaultVariants: {\n shape: 'circle',\n size: 'default',\n },\n }\n);\n\nexport type AvatarProps = VariantProps<typeof avatarVariants>;\n\n@Component({\n selector: 'tolle-avatar',\n standalone: true,\n imports: [NgIf],\n template: `\n <!-- Image Layer -->\n <img *ngIf=\"src && !hasError\"\n [src]=\"src\"\n [alt]=\"alt\"\n (load)=\"onLoad()\"\n (error)=\"onError()\"\n [class.opacity-0]=\"isLoading\"\n class=\"h-full w-full object-cover transition-opacity duration-300\" />\n\n <!-- Fallback Layer -->\n <div *ngIf=\"hasError || !src || isLoading\" class=\"flex h-full w-full items-center justify-center bg-muted\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AvatarComponent {\n /** Image URL. When absent or failed to load, projected fallback content is shown. */\n @Input() src?: string;\n /** Alternative text for the avatar image. @default '' */\n @Input() alt: string = '';\n /** Size of the avatar. @default 'default' */\n @Input() size: AvatarProps['size'] = 'default';\n /** Shape of the avatar. @default 'circle' */\n @Input() shape: AvatarProps['shape'] = 'circle';\n /** Extra Tailwind classes merged onto the avatar via `cn()` (last-wins). */\n @Input() class = '';\n\n isLoading = true;\n hasError = false;\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n onLoad() {\n this.isLoading = false;\n this.cdr.detectChanges();\n }\n\n onError() {\n this.isLoading = false;\n this.hasError = true;\n this.cdr.detectChanges();\n }\n\n // Apply styles directly to the <tolle-avatar> tag\n @HostBinding('class')\n get hostClasses() {\n return cn(avatarVariants({ shape: this.shape, size: this.size }), this.class);\n }\n}\n"
15
+ "content": "import { ChangeDetectorRef, Component, Input, HostBinding } from '@angular/core';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\nimport { NgIf } from '@angular/common';\n\nconst avatarVariants = cva(\n 'relative flex shrink-0 overflow-hidden bg-muted',\n {\n variants: {\n shape: {\n circle: 'rounded-full',\n square: 'rounded-md',\n },\n size: {\n sm: 'h-8 w-8 text-xs',\n default: 'h-10 w-10',\n lg: 'h-16 w-16 text-lg',\n xl: 'h-24 w-24 text-xl',\n },\n },\n defaultVariants: {\n shape: 'circle',\n size: 'default',\n },\n }\n);\n\nexport type AvatarProps = VariantProps<typeof avatarVariants>;\n\n@Component({\n selector: 'tolle-avatar',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [NgIf],\n template: `\n <!-- Image Layer -->\n <img *ngIf=\"src && !hasError\"\n [src]=\"src\"\n [alt]=\"alt\"\n (load)=\"onLoad()\"\n (error)=\"onError()\"\n [class.opacity-0]=\"isLoading\"\n class=\"h-full w-full object-cover transition-opacity duration-300\" />\n\n <!-- Fallback Layer -->\n <div *ngIf=\"hasError || !src || isLoading\" class=\"flex h-full w-full items-center justify-center bg-muted\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class AvatarComponent {\n /** Image URL. When absent or failed to load, projected fallback content is shown. */\n @Input() src?: string;\n /** Alternative text for the avatar image. @default '' */\n @Input() alt: string = '';\n /** Size of the avatar. @default 'default' */\n @Input() size: AvatarProps['size'] = 'default';\n /** Shape of the avatar. @default 'circle' */\n @Input() shape: AvatarProps['shape'] = 'circle';\n /** Extra Tailwind classes merged onto the avatar via `cn()` (last-wins). */\n @Input() class = '';\n\n isLoading = true;\n hasError = false;\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n onLoad() {\n this.isLoading = false;\n this.cdr.detectChanges();\n }\n\n onError() {\n this.isLoading = false;\n this.hasError = true;\n this.cdr.detectChanges();\n }\n\n // Apply styles directly to the <tolle-avatar> tag\n @HostBinding('class')\n get hostClasses() {\n return cn(avatarVariants({ shape: this.shape, size: this.size }), this.class);\n }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "badge.component.ts",
14
14
  "target": "ui/badge.component.ts",
15
- "content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst badgeVariants = cva(\n 'inline-flex w-fit items-center justify-center rounded-md border px-2 py-0.5 font-medium transition-colors gap-1 whitespace-nowrap',\n {\n variants: {\n variant: {\n default: 'border-transparent bg-primary text-primary-foreground',\n secondary: 'border-transparent bg-secondary text-secondary-foreground',\n outline: 'text-foreground border-border bg-transparent',\n destructive: 'border-transparent bg-destructive text-destructive-foreground',\n },\n size: {\n xs: 'px-1.5 py-0 text-[10px]',\n sm: 'px-2 py-0 text-[11px]',\n default: 'text-xs',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nexport type BadgeProps = VariantProps<typeof badgeVariants>;\n\n@Component({\n selector: 'tolle-badge',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"computedClass\">\n <ng-content select=\"[prefix]\"></ng-content>\n\n <span class=\"truncate\">\n <ng-content></ng-content>\n </span>\n\n <button\n *ngIf=\"removable\"\n type=\"button\"\n [attr.aria-label]=\"removeLabel\"\n (click)=\"onRemove.emit($event)\"\n class=\"ml-1 -mr-1 rounded-md transition-colors hover:bg-foreground/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\">\n <i class=\"ri-close-line\" aria-hidden=\"true\"></i>\n </button>\n </div>\n `,\n})\nexport class BadgeComponent {\n /** Visual style of the badge. @default 'default' */\n @Input() variant: BadgeProps['variant'] = 'default';\n /** Size of the badge. @default 'default' */\n @Input() size: BadgeProps['size'] = 'default';\n /** Shows a trailing dismiss (×) button that emits `onRemove`. @default false */\n @Input() removable = false;\n /** Accessible label for the dismiss button. @default 'Remove' */\n @Input() removeLabel = 'Remove';\n /** Extra Tailwind classes merged onto the badge via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the click event when the dismiss button is pressed. */\n @Output() onRemove = new EventEmitter<MouseEvent>();\n\n get computedClass() {\n return cn(\n badgeVariants({ variant: this.variant, size: this.size }),\n this.class\n );\n }\n}\n"
15
+ "content": "import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst badgeVariants = cva(\n 'inline-flex w-fit items-center justify-center rounded-md border px-2 py-0.5 font-medium transition-colors gap-1 whitespace-nowrap',\n {\n variants: {\n variant: {\n default: 'border-transparent bg-primary text-primary-foreground',\n secondary: 'border-transparent bg-secondary text-secondary-foreground',\n outline: 'text-foreground border-border bg-transparent',\n destructive: 'border-transparent bg-destructive text-destructive-foreground',\n },\n size: {\n xs: 'px-1.5 py-0 text-[10px]',\n sm: 'px-2 py-0 text-[11px]',\n default: 'text-xs',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nexport type BadgeProps = VariantProps<typeof badgeVariants>;\n\n@Component({\n selector: 'tolle-badge',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"computedClass\">\n <ng-content select=\"[prefix]\"></ng-content>\n\n <span class=\"truncate\">\n <ng-content></ng-content>\n </span>\n\n <button\n *ngIf=\"removable\"\n type=\"button\"\n [attr.aria-label]=\"removeLabel\"\n (click)=\"onRemove.emit($event)\"\n class=\"ml-1 -mr-1 rounded-md transition-colors hover:bg-foreground/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\">\n <i class=\"ri-close-line\" aria-hidden=\"true\"></i>\n </button>\n </div>\n `,\n})\nexport class BadgeComponent {\n /** Visual style of the badge. @default 'default' */\n @Input() variant: BadgeProps['variant'] = 'default';\n /** Size of the badge. @default 'default' */\n @Input() size: BadgeProps['size'] = 'default';\n /** Shows a trailing dismiss (×) button that emits `onRemove`. @default false */\n @Input() removable = false;\n /** Accessible label for the dismiss button. @default 'Remove' */\n @Input() removeLabel = 'Remove';\n /** Extra Tailwind classes merged onto the badge via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the click event when the dismiss button is pressed. */\n @Output() onRemove = new EventEmitter<MouseEvent>();\n\n get computedClass() {\n return cn(\n badgeVariants({ variant: this.variant, size: this.size }),\n this.class\n );\n }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "breadcrumb-item.component.ts",
12
12
  "target": "ui/breadcrumb-item.component.ts",
13
- "content": "import {Component, inject, OnInit} from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-item',\n standalone: true,\n imports: [],\n template: `\n <li class=\"inline-flex items-center gap-1.5\">\n <ng-content></ng-content>\n </li>\n `,\n styles: ``\n})\nexport class BreadcrumbItemComponent {\n\n}\n"
13
+ "content": "import {Component, inject, OnInit} from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-item',\n standalone: true,\n imports: [],\n template: `\n <li class=\"inline-flex items-center gap-1.5\">\n <ng-content></ng-content>\n </li>\n `,\n styles: `:host { display: inline-flex; }`\n})\nexport class BreadcrumbItemComponent {\n\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "breadcrumb-link.component.ts",
12
12
  "target": "ui/breadcrumb-link.component.ts",
13
- "content": "import {Component, Input} from '@angular/core';\nimport {NgIf, NgTemplateOutlet} from '@angular/common';\n\n@Component({\n selector: 'tolle-breadcrumb-link',\n standalone: true,\n imports: [\n NgIf,\n NgTemplateOutlet\n ],\n template: `\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n\n <ng-container *ngIf=\"active; else linkTemplate\">\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n class=\"font-normal text-foreground\"\n >\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </span>\n </ng-container>\n\n <ng-template #linkTemplate>\n <a class=\"transition-colors hover:text-foreground cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </a>\n </ng-template>\n `,\n styles: ``\n})\nexport class BreadcrumbLinkComponent {\n @Input() active: boolean = false;\n}\n"
13
+ "content": "import {Component, Input} from '@angular/core';\nimport {NgIf, NgTemplateOutlet} from '@angular/common';\n\n@Component({\n selector: 'tolle-breadcrumb-link',\n standalone: true,\n imports: [\n NgIf,\n NgTemplateOutlet\n ],\n template: `\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n\n <ng-container *ngIf=\"active; else linkTemplate\">\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n class=\"font-normal text-foreground\"\n >\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </span>\n </ng-container>\n\n <ng-template #linkTemplate>\n <a class=\"transition-colors hover:text-foreground cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </a>\n </ng-template>\n `,\n styles: `:host { display: inline-flex; }`\n})\nexport class BreadcrumbLinkComponent {\n @Input() active: boolean = false;\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "breadcrumb-separator.component.ts",
12
12
  "target": "ui/breadcrumb-separator.component.ts",
13
- "content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-separator',\n standalone: true,\n imports: [],\n template: `\n <li role=\"presentation\" aria-hidden=\"true\" class=\"[&>i]:size-3.5\">\n <ng-content>\n <i class=\"ri-arrow-right-s-line\"></i>\n </ng-content>\n </li>\n `,\n styles: ``\n})\nexport class BreadcrumbSeparatorComponent {\n\n}\n"
13
+ "content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-breadcrumb-separator',\n standalone: true,\n imports: [],\n template: `\n <li role=\"presentation\" aria-hidden=\"true\" class=\"[&>i]:size-3.5\">\n <ng-content>\n <i class=\"ri-arrow-right-s-line\"></i>\n </ng-content>\n </li>\n `,\n styles: `:host { display: inline-flex; }`\n})\nexport class BreadcrumbSeparatorComponent {\n\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "breadcrumb.component.ts",
12
12
  "target": "ui/breadcrumb.component.ts",
13
- "content": "import {Component, Input} from '@angular/core';\nimport { cn } from \"./utils/cn\";\n\n@Component({\n selector: 'tolle-breadcrumb',\n standalone: true,\n imports: [],\n template: `\n <nav aria-label=\"breadcrumb\" [class]=\"cn('flex flex-wrap items-center break-words text-sm text-muted-foreground', class)\">\n <ol class=\"flex flex-wrap items-center gap-1.5 break-words\">\n <ng-content></ng-content>\n </ol>\n </nav>\n `,\n styles: ``\n})\nexport class BreadcrumbComponent {\n @Input() class: string = '';\n protected cn = cn;\n}\n"
13
+ "content": "import {Component, Input} from '@angular/core';\nimport { cn } from \"./utils/cn\";\n\n@Component({\n selector: 'tolle-breadcrumb',\n standalone: true,\n imports: [],\n template: `\n <nav aria-label=\"breadcrumb\" [class]=\"cn('flex flex-wrap items-center break-words text-sm text-muted-foreground', class)\">\n <ol class=\"flex flex-wrap items-center gap-1.5 break-words\">\n <ng-content></ng-content>\n </ol>\n </nav>\n `,\n styles: `:host { display: block; }`\n})\nexport class BreadcrumbComponent {\n @Input() class: string = '';\n protected cn = cn;\n}\n"
14
14
  },
15
15
  {
16
16
  "path": "utils/cn.ts",
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "bubble",
3
+ "type": "registry:ui",
4
+ "title": "Bubble",
5
+ "description": "Bubble component with variant and align and size options.",
6
+ "category": "ai",
7
+ "registryDependencies": [],
8
+ "dependencies": [
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "bubble.component.ts",
14
+ "target": "ui/bubble.component.ts",
15
+ "content": "import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst bubbleVariants = cva(\n 'group/bubble relative w-fit max-w-full whitespace-pre-wrap break-words rounded-lg px-3 py-2 text-sm leading-relaxed transition-colors',\n {\n variants: {\n variant: {\n default: 'border border-border bg-card text-card-foreground',\n primary: 'border border-transparent bg-primary text-primary-foreground',\n muted: 'border border-transparent bg-muted text-foreground',\n outline: 'border border-border bg-transparent text-foreground',\n },\n align: {\n // The corner nearest the sender is squared off to read as a tail.\n start: 'rounded-bl-sm',\n end: 'rounded-br-sm',\n },\n size: {\n sm: 'px-2.5 py-1.5 text-xs',\n default: 'px-3 py-2 text-sm',\n lg: 'px-4 py-3 text-base',\n },\n },\n defaultVariants: { variant: 'default', align: 'start', size: 'default' },\n }\n);\n\nexport type BubbleProps = VariantProps<typeof bubbleVariants>;\n\n/** A single emoji reaction chip and its tally. */\nexport interface BubbleReaction {\n /** The emoji character to display, e.g. a thumbs-up. */\n emoji: string;\n /** How many people reacted with it. */\n count?: number;\n /** Whether the current user is one of them. */\n reacted?: boolean;\n}\n\n/**\n * The visible surface of a chat message — the rounded panel the text sits in.\n * Place it inside `tolle-message-content` and match its `align` to the message.\n * @new\n */\n@Component({\n selector: 'tolle-bubble',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div\n [class]=\"computedClass\"\n [attr.role]=\"interactive ? 'button' : null\"\n [attr.tabindex]=\"interactive ? 0 : null\"\n (click)=\"onTrigger($event)\"\n (keydown.enter)=\"onTrigger($event)\"\n (keydown.space)=\"onTrigger($event)\">\n <!-- Single content slot: collapsing only swaps the wrapper's classes so the\n projected nodes are never re-created (and never render empty). -->\n <div [class]=\"contentClass\" [style.max-height]=\"contentStyle\">\n <ng-content></ng-content>\n </div>\n\n <button\n *ngIf=\"collapsible\"\n type=\"button\"\n [attr.aria-expanded]=\"!collapsed\"\n (click)=\"toggle($event)\"\n class=\"mt-1 inline-flex items-center gap-1 rounded-sm text-xs font-medium underline underline-offset-2 opacity-80 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n {{ collapsed ? expandLabel : collapseLabel }}\n <i [class]=\"collapsed ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'\" aria-hidden=\"true\"></i>\n </button>\n\n <ng-content select=\"[bubbleFooter]\"></ng-content>\n </div>\n `,\n})\nexport class BubbleComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Visual style of the surface. @default 'default' */\n @Input() variant: BubbleProps['variant'] = 'default';\n /** Which edge the bubble hangs off; squares the matching bottom corner. @default 'start' */\n @Input() align: BubbleProps['align'] = 'start';\n /** Padding and text scale of the surface. @default 'default' */\n @Input() size: BubbleProps['size'] = 'default';\n /** Shows a show-more / show-less toggle for long content. @default false */\n @Input() collapsible = false;\n /** Whether the content is currently clamped. Only has an effect with `collapsible`. @default true */\n @Input() collapsed = true;\n /** Max height of the clamped content, as a CSS length. @default '6rem' */\n @Input() collapsedHeight = '6rem';\n /** Label of the toggle while the content is clamped. @default 'Show more' */\n @Input() expandLabel = 'Show more';\n /** Label of the toggle while the content is expanded. @default 'Show less' */\n @Input() collapseLabel = 'Show less';\n /** Makes the whole surface a keyboard-focusable trigger that emits `bubbleClick`. @default false */\n @Input() interactive = false;\n /** Extra Tailwind classes merged onto the surface via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the new collapsed state when the show-more toggle is pressed. */\n @Output() collapsedChange = new EventEmitter<boolean>();\n /** Emitted when an interactive bubble is activated by click, Enter, or Space. */\n @Output() bubbleClick = new EventEmitter<Event>();\n\n get computedClass(): string {\n return cn(\n bubbleVariants({ variant: this.variant, align: this.align, size: this.size }),\n this.interactive &&\n 'cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n this.class\n );\n }\n\n /** Classes for the content wrapper — clamped only while collapsible and collapsed. */\n get contentClass(): string {\n return cn(this.isClamped && 'overflow-hidden');\n }\n\n /** Inline max-height applied while the content is clamped. */\n get contentStyle(): string | null {\n return this.isClamped ? this.collapsedHeight : null;\n }\n\n /** Whether the projected content is currently clamped. */\n get isClamped(): boolean {\n return this.collapsible && this.collapsed;\n }\n\n protected toggle(event: Event): void {\n // The toggle lives inside the trigger surface; don't fire both.\n event.stopPropagation();\n this.collapsed = !this.collapsed;\n this.collapsedChange.emit(this.collapsed);\n }\n\n protected onTrigger(event: Event): void {\n if (!this.interactive) return;\n if (event.type === 'keydown') event.preventDefault();\n this.bubbleClick.emit(event);\n }\n}\n\n/**\n * Hover-revealed row of actions for a bubble — copy, retry, react. Clicks are\n * kept inside the row so an interactive bubble does not also fire.\n */\n@Component({\n selector: 'tolle-bubble-actions',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"computedClass\" (click)=\"onClick($event)\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class BubbleActionsComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Keeps the row permanently visible instead of revealing it on hover or focus. @default false */\n @Input() alwaysVisible = false;\n /** Extra Tailwind classes merged onto the row via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass(): string {\n return cn(\n 'mt-1 flex items-center gap-1 transition-opacity',\n this.alwaysVisible\n ? 'opacity-100'\n : 'opacity-0 focus-within:opacity-100 group-hover/bubble:opacity-100',\n this.class\n );\n }\n\n protected onClick(event: Event): void {\n // Actions stay independently clickable even when the bubble is a trigger.\n event.stopPropagation();\n }\n}\n\n/** Row of emoji reaction chips attached to a bubble. */\n@Component({\n selector: 'tolle-bubble-reactions',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"computedClass\" (click)=\"stop($event)\">\n <button\n *ngFor=\"let reaction of reactions; trackBy: trackByEmoji\"\n type=\"button\"\n [attr.aria-pressed]=\"!!reaction.reacted\"\n [class]=\"chipClass(reaction)\"\n (click)=\"onReact(reaction, $event)\">\n <span aria-hidden=\"true\">{{ reaction.emoji }}</span>\n <span *ngIf=\"reaction.count\" class=\"tabular-nums\">{{ reaction.count }}</span>\n </button>\n\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class BubbleReactionsComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Reaction chips to render, in display order. @default [] */\n @Input() reactions: BubbleReaction[] = [];\n /** Extra Tailwind classes merged onto the row via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the reaction whose chip was clicked. */\n @Output() react = new EventEmitter<BubbleReaction>();\n\n get computedClass(): string {\n return cn('mt-1 flex flex-wrap items-center gap-1', this.class);\n }\n\n protected chipClass(reaction: BubbleReaction): string {\n return cn(\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n reaction.reacted\n ? 'border-primary bg-primary/10 text-primary'\n : 'border-border bg-muted text-muted-foreground hover:bg-accent'\n );\n }\n\n protected trackByEmoji(_index: number, reaction: BubbleReaction): string {\n return reaction.emoji;\n }\n\n protected onReact(reaction: BubbleReaction, event: Event): void {\n event.stopPropagation();\n this.react.emit(reaction);\n }\n\n protected stop(event: Event): void {\n event.stopPropagation();\n }\n}\n"
16
+ },
17
+ {
18
+ "path": "utils/cn.ts",
19
+ "target": "ui/utils/cn.ts",
20
+ "content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
21
+ }
22
+ ]
23
+ }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "button.component.ts",
14
14
  "target": "ui/button.component.ts",
15
- "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst buttonVariants = cva(\n \"tolle-button-base inline-flex items-center justify-center rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background active:scale-[0.98]\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n xs: \"h-8 px-2 py-1 text-xs\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n \"icon-xs\": \"h-8 w-8\",\n \"icon-sm\": \"h-9 w-9\",\n icon: \"h-10 w-10\",\n \"icon-lg\": \"h-11 w-11\",\n },\n busy: { true: \"tolle-button--busy relative !cursor-wait !pointer-events-none\" }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport type ButtonProps = VariantProps<typeof buttonVariants>;\n\n@Component({\n selector: 'tolle-button',\n standalone: true,\n imports: [CommonModule],\n template: `\n <button\n [type]=\"type\"\n [class]=\"computedClass\"\n [disabled]=\"disabled || busy\"\n [attr.aria-busy]=\"busy\"\n >\n <div *ngIf=\"busy\" class=\"absolute inset-0 flex items-center justify-center\">\n <svg class=\"animate-spin h-5 w-5 text-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n\n <span class=\"flex items-center justify-center w-full h-full pointer-events-none\" [class.invisible]=\"busy\">\n <ng-content></ng-content>\n </span>\n </button>\n `,\n host: {\n 'class': 'tolle-button-wrapper inline-block align-middle'\n }\n})\nexport class ButtonComponent {\n /** Extra Tailwind classes merged onto the button via `cn()` (last-wins). */\n @Input() class: string = '';\n /** Visual style of the button. @default 'default' */\n @Input() variant: ButtonProps['variant'] = 'default';\n /** Size of the button, including icon-only sizes. @default 'default' */\n @Input() size: ButtonProps['size'] = 'default';\n /** Native button type. Defaults to 'button' so it never submits a form unexpectedly. @default 'button' */\n @Input() type: 'button' | 'submit' | 'reset' = 'button';\n /** Disables the button and blocks pointer events. @default false */\n @Input() disabled: boolean = false;\n /** Shows a spinner, sets `aria-busy`, and prevents interaction while a task runs. @default false */\n @Input() busy: boolean = false;\n\n get computedClass() {\n return cn(\n buttonVariants({\n variant: this.variant,\n size: this.size,\n busy: this.busy\n }),\n this.class\n );\n }\n}\n"
15
+ "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst buttonVariants = cva(\n \"tolle-button-base inline-flex items-center justify-center rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background active:scale-[0.98]\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n xs: \"h-8 px-2 py-1 text-xs\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n \"icon-xs\": \"h-8 w-8\",\n \"icon-sm\": \"h-9 w-9\",\n icon: \"h-10 w-10\",\n \"icon-lg\": \"h-11 w-11\",\n },\n busy: { true: \"tolle-button--busy relative !cursor-wait !pointer-events-none\" }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport type ButtonProps = VariantProps<typeof buttonVariants>;\n\n@Component({\n selector: 'tolle-button',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <button\n [type]=\"type\"\n [class]=\"computedClass\"\n [disabled]=\"disabled || busy\"\n [attr.aria-busy]=\"busy\"\n >\n <div *ngIf=\"busy\" class=\"absolute inset-0 flex items-center justify-center\">\n <svg class=\"animate-spin h-5 w-5 text-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n\n <span class=\"flex items-center justify-center w-full h-full pointer-events-none\" [class.invisible]=\"busy\">\n <ng-content></ng-content>\n </span>\n </button>\n `,\n host: {\n 'class': 'tolle-button-wrapper inline-block align-middle'\n }\n})\nexport class ButtonComponent {\n /** Extra Tailwind classes merged onto the button via `cn()` (last-wins). */\n @Input() class: string = '';\n /** Visual style of the button. @default 'default' */\n @Input() variant: ButtonProps['variant'] = 'default';\n /** Size of the button, including icon-only sizes. @default 'default' */\n @Input() size: ButtonProps['size'] = 'default';\n /** Native button type. Defaults to 'button' so it never submits a form unexpectedly. @default 'button' */\n @Input() type: 'button' | 'submit' | 'reset' = 'button';\n /** Disables the button and blocks pointer events. @default false */\n @Input() disabled: boolean = false;\n /** Shows a spinner, sets `aria-busy`, and prevents interaction while a task runs. @default false */\n @Input() busy: boolean = false;\n\n get computedClass() {\n return cn(\n buttonVariants({\n variant: this.variant,\n size: this.size,\n busy: this.busy\n }),\n this.class\n );\n }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "calendar.component.ts",
14
14
  "target": "ui/calendar.component.ts",
15
- "content": "import {\n Component, Input, OnInit, forwardRef, Output, EventEmitter, ChangeDetectorRef\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport {\n addDays, addMonths, subMonths, startOfMonth, endOfMonth,\n startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth,\n isSameDay, isToday, setMonth, setYear, addYears, subYears,\n isBefore, startOfDay, format\n} from 'date-fns';\nimport { cn } from './utils/cn';\n\nexport type CalendarMode = 'date' | 'month' | 'year';\n\n@Component({\n selector: 'tolle-calendar',\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => CalendarComponent),\n multi: true\n }\n ],\n template: `\n <div [class]=\"cn('text-popover-foreground inline-block min-w-fit', bordered ? 'p-3 border rounded-md bg-background shadow-sm' : '', class)\">\n <!-- Header with Navigation -->\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <!-- View Selector -->\n <div class=\"flex items-center gap-1\">\n <button *ngIf=\"mode !== 'year'\"\n type=\"button\"\n (click)=\"setView('month')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'month' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'month') }}\n </button>\n\n <button\n type=\"button\"\n (click)=\"setView('year')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'year' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'year') }}\n </button>\n </div>\n\n <!-- Navigation Buttons -->\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\">\n <i class=\"ri-arrow-left-s-line text-lg\"></i>\n </button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\">\n <i class=\"ri-arrow-right-s-line text-lg\"></i>\n </button>\n </div>\n </div>\n\n <!-- DATE MODE -->\n <div *ngIf=\"currentView === 'date' && mode === 'date'\" class=\"space-y-2 animate-in fade-in zoom-in-95 duration-200\">\n <div class=\"grid grid-cols-7 gap-1 w-full\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">\n {{ day }}\n </span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-1 w-full\" (keydown)=\"onGridKeydown($event)\">\n <button\n *ngFor=\"let date of daysInMonth\"\n type=\"button\"\n role=\"gridcell\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date)\"\n >\n {{ formatDate(date, 'day') }}\n </button>\n </div>\n </div>\n\n <!-- MONTH SELECTOR (for date mode and month mode) -->\n <div *ngIf=\"(currentView === 'month')\"\n class=\"grid grid-cols-3 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let month of months; let i = index\"\n type=\"button\"\n (click)=\"selectMonth(i)\"\n [class]=\"getMonthClass(i)\"\n >\n {{ month }}\n </button>\n </div>\n\n <!-- YEAR SELECTOR (for date mode and year mode) -->\n <div *ngIf=\"(currentView === 'year') \"\n class=\"grid grid-cols-4 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let year of years\"\n type=\"button\"\n (click)=\"selectYear(year)\"\n [class]=\"getYearClass(year)\"\n >\n {{ year }}\n </button>\n </div>\n\n <!-- Quick Actions -->\n <div *ngIf=\"showQuickActions\" class=\"border-t pt-3 mt-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <button\n type=\"button\"\n (click)=\"selectToday()\"\n [class]=\"quickActionBtnClass\"\n [disabled]=\"isTodayDisabled()\"\n >\n Today\n </button>\n <button\n type=\"button\"\n (click)=\"clear()\"\n [class]=\"quickActionBtnClass\"\n >\n Clear\n </button>\n </div>\n </div>\n </div>\n `\n})\nexport class CalendarComponent implements OnInit, ControlValueAccessor {\n @Input() class = '';\n /** Renders the calendar's own border, background and shadow. Set `false` when\n * nesting inside a card/popover that already provides the chrome. @default true */\n @Input() bordered = true;\n @Input() mode: CalendarMode = 'date';\n @Input() disablePastDates = false;\n @Input() showQuickActions = true;\n @Input() minDate?: Date;\n @Input() maxDate?: Date;\n @Input() formatMonthFn?: (date: Date) => string;\n @Input() formatYearFn?: (date: Date) => string;\n @Input() formatDateFn?: (date: Date) => string;\n\n @Output() dateSelect = new EventEmitter<Date | null>();\n\n currentView: 'date' | 'month' | 'year' = 'date';\n viewDate: Date = new Date();\n selectedDate: Date | null = null;\n\n weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];\n daysInMonth: Date[] = [];\n months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n years: number[] = [];\n yearRangeStart: number;\n\n navBtnClass = cn('h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-input rounded-md flex items-center justify-center hover:bg-accent hover:text-accent-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring');\n quickActionBtnClass = cn('px-3 py-1.5 text-sm rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors');\n\n onTouched: () => void = () => {};\n onChange: (value: any) => void = () => {};\n\n protected cn = cn;\n\n constructor(private cdr: ChangeDetectorRef) {\n this.yearRangeStart = new Date().getFullYear() - 6;\n }\n\n ngOnInit() {\n // Initialize based on mode\n if (this.mode === 'month') {\n this.currentView = 'month';\n } else if (this.mode === 'year') {\n this.currentView = 'year';\n }\n\n this.generateDays();\n this.generateYears();\n }\n\n // Format helpers\n formatMonthYear(date: Date, type: 'month' | 'year'): string {\n if (type === 'month' && this.formatMonthFn) {\n return this.formatMonthFn(date);\n }\n if (type === 'year' && this.formatYearFn) {\n return this.formatYearFn(date);\n }\n return type === 'month' ? format(date, 'MMMM') : format(date, 'yyyy');\n }\n\n formatDate(date: Date, type: 'day' | 'month' | 'year'): string {\n if (type === 'day' && this.formatDateFn) {\n return this.formatDateFn(date);\n }\n return format(date, type === 'day' ? 'd' : type === 'month' ? 'MMM' : 'yyyy');\n }\n\n generateDays() {\n if (this.mode !== 'date') return;\n\n const start = startOfWeek(startOfMonth(this.viewDate));\n const end = endOfWeek(endOfMonth(this.viewDate));\n this.daysInMonth = eachDayOfInterval({ start, end });\n }\n\n generateYears() {\n const currentYear = this.viewDate.getFullYear();\n\n if (this.mode === 'year') {\n // For year picker, show a 12-year grid\n this.years = Array.from({ length: 12 }, (_, i) => this.yearRangeStart + i);\n } else {\n // For date mode year selector, show 16 years centered on current\n this.years = Array.from({ length: 16 }, (_, i) => currentYear - 6 + i);\n }\n }\n\n setView(view: 'date' | 'month' | 'year') {\n this.currentView = view;\n if (view === 'year') {\n this.generateYears();\n }\n }\n\n prev() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = subMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = subYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart -= 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n }\n }\n\n next() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = addMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = addYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart += 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n }\n }\n\n prevYears() {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n\n nextYears() {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n\n selectDate(date: Date) {\n if (this.isDateDisabled(date)) return;\n\n this.selectedDate = date;\n this.onChange(date);\n this.onTouched();\n this.dateSelect.emit(date);\n }\n\n /**\n * Grid keyboard navigation for the day view (react-day-picker semantics).\n * Arrows move by day/week, Home/End to week edges, PageUp/PageDown by month.\n * Enter/Space are left to the native day <button> (which selects on click and\n * does not scroll the page).\n */\n onGridKeydown(event: KeyboardEvent) {\n if (this.currentView !== 'date' || this.mode !== 'date') return;\n\n const gridEl = event.currentTarget as HTMLElement;\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n const idx = buttons.indexOf(event.target as HTMLElement);\n if (idx < 0) return;\n\n const current = this.daysInMonth[idx];\n if (!current) return;\n\n let next: Date | null = null;\n switch (event.key) {\n case 'ArrowLeft': next = addDays(current, -1); break;\n case 'ArrowRight': next = addDays(current, 1); break;\n case 'ArrowUp': next = addDays(current, -7); break;\n case 'ArrowDown': next = addDays(current, 7); break;\n case 'Home': next = startOfWeek(current); break;\n case 'End': next = endOfWeek(current); break;\n case 'PageUp': next = subMonths(current, 1); break;\n case 'PageDown': next = addMonths(current, 1); break;\n default: return;\n }\n\n event.preventDefault();\n this.focusDay(next, gridEl);\n }\n\n /**\n * Moves focus to the day button matching `date`. If the day is not present in\n * the current grid, the view advances/retreats to its month, regenerates and\n * then focuses it.\n */\n private focusDay(date: Date, gridEl: HTMLElement) {\n let idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n if (idx < 0) {\n this.viewDate = startOfMonth(date);\n this.generateDays();\n this.cdr.detectChanges(); // re-render the grid synchronously\n idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n }\n if (idx < 0) return;\n\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n buttons[idx]?.focus();\n }\n\n selectMonth(monthIndex: number) {\n if (this.mode === 'date') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'month') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n selectYear(year: number) {\n if (this.mode === 'date') {\n this.viewDate = setYear(this.viewDate, year);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'year' || this.mode === 'month') {\n this.viewDate = setYear(this.viewDate, year);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n isSelected(date: Date): boolean {\n return !!(this.selectedDate && isSameDay(date, this.selectedDate));\n }\n\n getDayClass(date: Date) {\n const isSelected = this.selectedDate && isSameDay(date, this.selectedDate);\n const isTodayDate = isToday(date);\n const isOutside = !isSameMonth(date, this.viewDate);\n const isDisabled = this.isDateDisabled(date);\n\n return cn(\n 'h-9 w-9 p-0 font-normal text-sm rounded-md transition-all flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n !isSelected && !isDisabled && 'hover:bg-accent hover:text-accent-foreground',\n isSelected && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground',\n !isSelected && isTodayDate && 'bg-accent text-accent-foreground',\n (isOutside || isDisabled) && 'text-muted-foreground opacity-50',\n isDisabled && 'cursor-not-allowed'\n );\n }\n\n getMonthClass(monthIndex: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getMonth() === monthIndex &&\n this.selectedDate.getFullYear() === this.viewDate.getFullYear();\n const isCurrent = new Date().getMonth() === monthIndex &&\n new Date().getFullYear() === this.viewDate.getFullYear();\n\n return cn(\n 'text-sm py-2.5 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n getYearClass(year: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getFullYear() === year;\n const isCurrent = new Date().getFullYear() === year;\n\n return cn(\n 'text-sm py-2 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n isDateDisabled(date: Date): boolean {\n if (this.disablePastDates && isBefore(date, startOfDay(new Date()))) {\n return true;\n }\n if (this.minDate && isBefore(date, this.minDate)) {\n return true;\n }\n return !!(this.maxDate && isBefore(this.maxDate, date));\n\n }\n\n isTodayDisabled(): boolean {\n return this.isDateDisabled(new Date());\n }\n\n selectToday() {\n if (!this.isTodayDisabled()) {\n this.selectDate(new Date());\n }\n }\n\n clear() {\n this.selectedDate = null;\n this.onChange(null);\n this.onTouched();\n this.dateSelect.emit(null);\n }\n\n // CVA Implementation\n writeValue(obj: any): void {\n if (obj) {\n const date = new Date(obj);\n if (!isNaN(date.getTime())) {\n this.selectedDate = date;\n this.viewDate = date;\n this.generateDays();\n this.generateYears();\n }\n }\n }\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n}\n"
15
+ "content": "import {\n Component, Input, OnInit, forwardRef, Output, EventEmitter, ChangeDetectorRef\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport {\n addDays, addMonths, subMonths, startOfMonth, endOfMonth,\n startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth,\n isSameDay, isToday, setMonth, setYear, addYears, subYears,\n isBefore, startOfDay, format\n} from 'date-fns';\nimport { cn } from './utils/cn';\n\nexport type CalendarMode = 'date' | 'month' | 'year';\n\n@Component({\n selector: 'tolle-calendar',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => CalendarComponent),\n multi: true\n }\n ],\n template: `\n <div [class]=\"cn('text-popover-foreground inline-block min-w-fit', bordered ? 'p-3 border rounded-md bg-background shadow-sm' : '', class)\">\n <!-- Header with Navigation -->\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <!-- View Selector -->\n <div class=\"flex items-center gap-1\">\n <button *ngIf=\"mode !== 'year'\"\n type=\"button\"\n (click)=\"setView('month')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'month' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'month') }}\n </button>\n\n <button\n type=\"button\"\n (click)=\"setView('year')\"\n [class]=\"cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n currentView === 'year' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n )\">\n {{ formatMonthYear(viewDate, 'year') }}\n </button>\n </div>\n\n <!-- Navigation Buttons -->\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\">\n <i class=\"ri-arrow-left-s-line text-lg\"></i>\n </button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\">\n <i class=\"ri-arrow-right-s-line text-lg\"></i>\n </button>\n </div>\n </div>\n\n <!-- DATE MODE -->\n <div *ngIf=\"currentView === 'date' && mode === 'date'\" class=\"space-y-2 animate-in fade-in zoom-in-95 duration-200\">\n <div class=\"grid grid-cols-7 gap-1 w-full\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">\n {{ day }}\n </span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-1 w-full\" (keydown)=\"onGridKeydown($event)\">\n <button\n *ngFor=\"let date of daysInMonth\"\n type=\"button\"\n role=\"gridcell\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date)\"\n >\n {{ formatDate(date, 'day') }}\n </button>\n </div>\n </div>\n\n <!-- MONTH SELECTOR (for date mode and month mode) -->\n <div *ngIf=\"(currentView === 'month')\"\n class=\"grid grid-cols-3 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let month of months; let i = index\"\n type=\"button\"\n (click)=\"selectMonth(i)\"\n [class]=\"getMonthClass(i)\"\n >\n {{ month }}\n </button>\n </div>\n\n <!-- YEAR SELECTOR (for date mode and year mode) -->\n <div *ngIf=\"(currentView === 'year') \"\n class=\"grid grid-cols-4 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button\n *ngFor=\"let year of years\"\n type=\"button\"\n (click)=\"selectYear(year)\"\n [class]=\"getYearClass(year)\"\n >\n {{ year }}\n </button>\n </div>\n\n <!-- Quick Actions -->\n <div *ngIf=\"showQuickActions\" class=\"border-t pt-3 mt-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <button\n type=\"button\"\n (click)=\"selectToday()\"\n [class]=\"quickActionBtnClass\"\n [disabled]=\"isTodayDisabled()\"\n >\n Today\n </button>\n <button\n type=\"button\"\n (click)=\"clear()\"\n [class]=\"quickActionBtnClass\"\n >\n Clear\n </button>\n </div>\n </div>\n </div>\n `\n})\nexport class CalendarComponent implements OnInit, ControlValueAccessor {\n @Input() class = '';\n /** Renders the calendar's own border, background and shadow. Set `false` when\n * nesting inside a card/popover that already provides the chrome. @default true */\n @Input() bordered = true;\n @Input() mode: CalendarMode = 'date';\n @Input() disablePastDates = false;\n @Input() showQuickActions = true;\n @Input() minDate?: Date;\n @Input() maxDate?: Date;\n @Input() formatMonthFn?: (date: Date) => string;\n @Input() formatYearFn?: (date: Date) => string;\n @Input() formatDateFn?: (date: Date) => string;\n\n @Output() dateSelect = new EventEmitter<Date | null>();\n\n currentView: 'date' | 'month' | 'year' = 'date';\n viewDate: Date = new Date();\n selectedDate: Date | null = null;\n\n weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];\n daysInMonth: Date[] = [];\n months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n years: number[] = [];\n yearRangeStart: number;\n\n navBtnClass = cn('h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-input rounded-md flex items-center justify-center hover:bg-accent hover:text-accent-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring');\n quickActionBtnClass = cn('px-3 py-1.5 text-sm rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors');\n\n onTouched: () => void = () => {};\n onChange: (value: any) => void = () => {};\n\n protected cn = cn;\n\n constructor(private cdr: ChangeDetectorRef) {\n this.yearRangeStart = new Date().getFullYear() - 6;\n }\n\n ngOnInit() {\n // Initialize based on mode\n if (this.mode === 'month') {\n this.currentView = 'month';\n } else if (this.mode === 'year') {\n this.currentView = 'year';\n }\n\n this.generateDays();\n this.generateYears();\n }\n\n // Format helpers\n formatMonthYear(date: Date, type: 'month' | 'year'): string {\n if (type === 'month' && this.formatMonthFn) {\n return this.formatMonthFn(date);\n }\n if (type === 'year' && this.formatYearFn) {\n return this.formatYearFn(date);\n }\n return type === 'month' ? format(date, 'MMMM') : format(date, 'yyyy');\n }\n\n formatDate(date: Date, type: 'day' | 'month' | 'year'): string {\n if (type === 'day' && this.formatDateFn) {\n return this.formatDateFn(date);\n }\n return format(date, type === 'day' ? 'd' : type === 'month' ? 'MMM' : 'yyyy');\n }\n\n generateDays() {\n if (this.mode !== 'date') return;\n\n const start = startOfWeek(startOfMonth(this.viewDate));\n const end = endOfWeek(endOfMonth(this.viewDate));\n this.daysInMonth = eachDayOfInterval({ start, end });\n }\n\n generateYears() {\n const currentYear = this.viewDate.getFullYear();\n\n if (this.mode === 'year') {\n // For year picker, show a 12-year grid\n this.years = Array.from({ length: 12 }, (_, i) => this.yearRangeStart + i);\n } else {\n // For date mode year selector, show 16 years centered on current\n this.years = Array.from({ length: 16 }, (_, i) => currentYear - 6 + i);\n }\n }\n\n setView(view: 'date' | 'month' | 'year') {\n this.currentView = view;\n if (view === 'year') {\n this.generateYears();\n }\n }\n\n prev() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = subMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = subYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart -= 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n } else {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n }\n }\n\n next() {\n if (this.mode === 'date') {\n if (this.currentView === 'date') {\n this.viewDate = addMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = addYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n }\n } else if (this.mode === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else if (this.mode === 'year') {\n this.yearRangeStart += 12;\n this.generateYears();\n } else if (this.mode === 'month-year') {\n if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n } else {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n }\n }\n\n prevYears() {\n this.yearRangeStart -= 12;\n this.generateYears();\n }\n\n nextYears() {\n this.yearRangeStart += 12;\n this.generateYears();\n }\n\n selectDate(date: Date) {\n if (this.isDateDisabled(date)) return;\n\n this.selectedDate = date;\n this.onChange(date);\n this.onTouched();\n this.dateSelect.emit(date);\n }\n\n /**\n * Grid keyboard navigation for the day view (react-day-picker semantics).\n * Arrows move by day/week, Home/End to week edges, PageUp/PageDown by month.\n * Enter/Space are left to the native day <button> (which selects on click and\n * does not scroll the page).\n */\n onGridKeydown(event: KeyboardEvent) {\n if (this.currentView !== 'date' || this.mode !== 'date') return;\n\n const gridEl = event.currentTarget as HTMLElement;\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n const idx = buttons.indexOf(event.target as HTMLElement);\n if (idx < 0) return;\n\n const current = this.daysInMonth[idx];\n if (!current) return;\n\n let next: Date | null = null;\n switch (event.key) {\n case 'ArrowLeft': next = addDays(current, -1); break;\n case 'ArrowRight': next = addDays(current, 1); break;\n case 'ArrowUp': next = addDays(current, -7); break;\n case 'ArrowDown': next = addDays(current, 7); break;\n case 'Home': next = startOfWeek(current); break;\n case 'End': next = endOfWeek(current); break;\n case 'PageUp': next = subMonths(current, 1); break;\n case 'PageDown': next = addMonths(current, 1); break;\n default: return;\n }\n\n event.preventDefault();\n this.focusDay(next, gridEl);\n }\n\n /**\n * Moves focus to the day button matching `date`. If the day is not present in\n * the current grid, the view advances/retreats to its month, regenerates and\n * then focuses it.\n */\n private focusDay(date: Date, gridEl: HTMLElement) {\n let idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n if (idx < 0) {\n this.viewDate = startOfMonth(date);\n this.generateDays();\n this.cdr.detectChanges(); // re-render the grid synchronously\n idx = this.daysInMonth.findIndex(d => isSameDay(d, date));\n }\n if (idx < 0) return;\n\n const buttons = Array.from(gridEl.querySelectorAll('button')) as HTMLElement[];\n buttons[idx]?.focus();\n }\n\n selectMonth(monthIndex: number) {\n if (this.mode === 'date') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'month') {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n selectYear(year: number) {\n if (this.mode === 'date') {\n this.viewDate = setYear(this.viewDate, year);\n this.currentView = 'date';\n this.generateDays();\n } else if (this.mode === 'year' || this.mode === 'month') {\n this.viewDate = setYear(this.viewDate, year);\n this.selectedDate = this.viewDate;\n this.onChange( this.viewDate);\n this.onTouched();\n this.dateSelect.emit( this.viewDate);\n }\n }\n\n isSelected(date: Date): boolean {\n return !!(this.selectedDate && isSameDay(date, this.selectedDate));\n }\n\n getDayClass(date: Date) {\n const isSelected = this.selectedDate && isSameDay(date, this.selectedDate);\n const isTodayDate = isToday(date);\n const isOutside = !isSameMonth(date, this.viewDate);\n const isDisabled = this.isDateDisabled(date);\n\n return cn(\n 'h-9 w-9 p-0 font-normal text-sm rounded-md transition-all flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n !isSelected && !isDisabled && 'hover:bg-accent hover:text-accent-foreground',\n isSelected && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground',\n !isSelected && isTodayDate && 'bg-accent text-accent-foreground',\n (isOutside || isDisabled) && 'text-muted-foreground opacity-50',\n isDisabled && 'cursor-not-allowed'\n );\n }\n\n getMonthClass(monthIndex: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getMonth() === monthIndex &&\n this.selectedDate.getFullYear() === this.viewDate.getFullYear();\n const isCurrent = new Date().getMonth() === monthIndex &&\n new Date().getFullYear() === this.viewDate.getFullYear();\n\n return cn(\n 'text-sm py-2.5 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n getYearClass(year: number) {\n const isSelected = this.selectedDate &&\n this.selectedDate.getFullYear() === year;\n const isCurrent = new Date().getFullYear() === year;\n\n return cn(\n 'text-sm py-2 rounded-md transition-colors',\n isSelected ? 'bg-primary text-primary-foreground hover:bg-primary' :\n isCurrent ? 'border border-primary/30 text-primary' :\n 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n isDateDisabled(date: Date): boolean {\n if (this.disablePastDates && isBefore(date, startOfDay(new Date()))) {\n return true;\n }\n if (this.minDate && isBefore(date, this.minDate)) {\n return true;\n }\n return !!(this.maxDate && isBefore(this.maxDate, date));\n\n }\n\n isTodayDisabled(): boolean {\n return this.isDateDisabled(new Date());\n }\n\n selectToday() {\n if (!this.isTodayDisabled()) {\n this.selectDate(new Date());\n }\n }\n\n clear() {\n this.selectedDate = null;\n this.onChange(null);\n this.onTouched();\n this.dateSelect.emit(null);\n }\n\n // CVA Implementation\n writeValue(obj: any): void {\n if (obj) {\n const date = new Date(obj);\n if (!isNaN(date.getTime())) {\n this.selectedDate = date;\n this.viewDate = date;\n this.generateDays();\n this.generateYears();\n }\n }\n }\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "utils/cn.ts",
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "path": "card.component.ts",
12
12
  "target": "ui/card.component.ts",
13
- "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-card',\n standalone: true,\n imports: [CommonModule],\n // Card owns the vertical rhythm: `flex-col` + `gap-6` spaces the sections and\n // `py-6` gives top/bottom padding. Sub-sections only add horizontal padding\n // (`px-6`). This means a card renders correctly with ANY combination of\n // header / content / footer — including content-only — with no `pt-0` hacks.\n template: `\n <div [class]=\"cn('flex flex-col gap-6 rounded-lg border border-border bg-card text-card-foreground py-6 shadow-sm', class)\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class CardComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-header',\n standalone: true,\n template: `<div [class]=\"cn('flex flex-col gap-1.5 px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardHeaderComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-title',\n standalone: true,\n template: `<h3 [class]=\"cn('font-semibold leading-none tracking-tight', class)\"><ng-content></ng-content></h3>`,\n})\nexport class CardTitleComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-description',\n standalone: true,\n template: `<p [class]=\"cn('text-sm text-muted-foreground', class)\"><ng-content></ng-content></p>`,\n})\nexport class CardDescriptionComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-content',\n standalone: true,\n template: `<div [class]=\"cn('px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardContentComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-footer',\n standalone: true,\n template: `<div [class]=\"cn('flex items-center px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardFooterComponent { @Input() class = ''; protected cn = cn; }\n"
13
+ "content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-card',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n // Card owns the vertical rhythm: `flex-col` + `gap-6` spaces the sections and\n // `py-6` gives top/bottom padding. Sub-sections only add horizontal padding\n // (`px-6`). This means a card renders correctly with ANY combination of\n // header / content / footer — including content-only — with no `pt-0` hacks.\n template: `\n <div [class]=\"cn('flex flex-col gap-6 rounded-lg border border-border bg-card text-card-foreground py-6 shadow-sm', class)\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class CardComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-header',\n styles: [':host { display: block; }'],\n standalone: true,\n template: `<div [class]=\"cn('flex flex-col gap-1.5 px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardHeaderComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-title',\n styles: [':host { display: block; }'],\n standalone: true,\n template: `<h3 [class]=\"cn('font-semibold leading-none tracking-tight', class)\"><ng-content></ng-content></h3>`,\n})\nexport class CardTitleComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-description',\n styles: [':host { display: block; }'],\n standalone: true,\n template: `<p [class]=\"cn('text-sm text-muted-foreground', class)\"><ng-content></ng-content></p>`,\n})\nexport class CardDescriptionComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-content',\n styles: [':host { display: block; }'],\n standalone: true,\n template: `<div [class]=\"cn('px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardContentComponent { @Input() class = ''; protected cn = cn; }\n\n@Component({\n selector: 'tolle-card-footer',\n styles: [':host { display: block; }'],\n standalone: true,\n template: `<div [class]=\"cn('flex items-center px-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class CardFooterComponent { @Input() class = ''; protected cn = cn; }\n"
14
14
  },
15
15
  {
16
16
  "path": "utils/cn.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "carousel/carousel.component.ts",
14
14
  "target": "ui/carousel/carousel.component.ts",
15
- "content": "import {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ViewChild,\n AfterViewInit,\n OnDestroy,\n OnInit,\n OnChanges,\n ContentChild,\n forwardRef,\n Injectable,\n Directive,\n HostListener,\n inject,\n ChangeDetectorRef,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport EmblaCarousel, {\n EmblaCarouselType,\n EmblaOptionsType,\n EmblaPluginType,\n} from 'embla-carousel';\nimport { Subject } from 'rxjs';\n\n@Injectable()\nexport class CarouselContext {\n api$ = new Subject<EmblaCarouselType>();\n api?: EmblaCarouselType;\n orientation: 'horizontal' | 'vertical' = 'horizontal';\n canScrollPrev = false;\n canScrollNext = false;\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n setApi(api: EmblaCarouselType) {\n this.api = api;\n this.api$.next(api);\n this.updateState();\n\n api.on('select', () => this.updateState());\n api.on('reInit', () => this.updateState());\n }\n\n updateState() {\n if (!this.api) return;\n this.canScrollPrev = this.api.canScrollPrev();\n this.canScrollNext = this.api.canScrollNext();\n this.cdr.detectChanges();\n }\n\n scrollPrev() {\n this.api?.scrollPrev();\n }\n\n scrollNext() {\n this.api?.scrollNext();\n }\n}\n\n@Component({\n selector: 'tolle-carousel',\n standalone: true,\n imports: [CommonModule],\n providers: [CarouselContext],\n template: `\n <div\n class=\"relative\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabindex=\"0\"\n (keydown)=\"onKeydown($event)\"\n [class.flex-col]=\"orientation === 'vertical'\"\n >\n <ng-content></ng-content>\n </div>\n `,\n host: {\n '[class.flex]': 'orientation === \"vertical\"',\n }\n})\nexport class CarouselComponent implements AfterViewInit, OnDestroy {\n @Input() opts: EmblaOptionsType = {};\n @Input() plugins: EmblaPluginType[] = [];\n @Input() orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n @Output() api = new EventEmitter<EmblaCarouselType>();\n\n private carouselContext = inject(CarouselContext);\n private emblaApi?: EmblaCarouselType;\n\n @ViewChild('container', { static: false }) containerRef?: ElementRef;\n\n constructor() { }\n\n ngOnInit() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngOnChanges() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngAfterViewInit() {\n // We expect tolleCarouselContent to provide the viewport element\n }\n\n onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n this.carouselContext.scrollPrev();\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n this.carouselContext.scrollNext();\n }\n }\n\n initApi(viewport: HTMLElement) {\n this.emblaApi = EmblaCarousel(viewport, {\n ...this.opts,\n axis: this.orientation === 'horizontal' ? 'x' : 'y',\n }, this.plugins);\n\n this.carouselContext.setApi(this.emblaApi);\n this.api.emit(this.emblaApi);\n }\n\n ngOnDestroy() {\n this.emblaApi?.destroy();\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContent]',\n standalone: true,\n host: {\n 'class': 'overflow-hidden',\n }\n})\nexport class CarouselContentDirective implements AfterViewInit {\n private carousel = inject(CarouselComponent);\n private el = inject(ElementRef);\n\n ngAfterViewInit() {\n this.carousel.initApi(this.el.nativeElement);\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContainer]',\n standalone: true,\n host: {\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"flex -ml-4\" : \"flex flex-col -mt-4\"',\n }\n})\nexport class CarouselContainerDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselItem]',\n standalone: true,\n host: {\n 'role': 'group',\n 'aria-roledescription': 'slide',\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"min-w-0 shrink-0 grow-0 basis-full pl-4\" : \"min-w-0 shrink-0 grow-0 basis-full pt-4\"',\n }\n})\nexport class CarouselItemDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselPrevious]',\n standalone: true,\n host: {\n 'aria-label': 'Previous slide',\n '[attr.disabled]': '!carouselContext.canScrollPrev ? true : null',\n '(click)': 'carouselContext.scrollPrev()',\n }\n})\nexport class CarouselPreviousDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselNext]',\n standalone: true,\n host: {\n 'aria-label': 'Next slide',\n '[attr.disabled]': '!carouselContext.canScrollNext ? true : null',\n '(click)': 'carouselContext.scrollNext()',\n }\n})\nexport class CarouselNextDirective {\n carouselContext = inject(CarouselContext);\n}\n"
15
+ "content": "import {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ViewChild,\n AfterViewInit,\n OnDestroy,\n OnInit,\n OnChanges,\n ContentChild,\n forwardRef,\n Injectable,\n Directive,\n HostListener,\n inject,\n ChangeDetectorRef,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport EmblaCarousel, {\n EmblaCarouselType,\n EmblaOptionsType,\n EmblaPluginType,\n} from 'embla-carousel';\nimport { Subject } from 'rxjs';\n\n@Injectable()\nexport class CarouselContext {\n api$ = new Subject<EmblaCarouselType>();\n api?: EmblaCarouselType;\n orientation: 'horizontal' | 'vertical' = 'horizontal';\n canScrollPrev = false;\n canScrollNext = false;\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n setApi(api: EmblaCarouselType) {\n this.api = api;\n this.api$.next(api);\n this.updateState();\n\n api.on('select', () => this.updateState());\n api.on('reInit', () => this.updateState());\n }\n\n updateState() {\n if (!this.api) return;\n this.canScrollPrev = this.api.canScrollPrev();\n this.canScrollNext = this.api.canScrollNext();\n this.cdr.detectChanges();\n }\n\n scrollPrev() {\n this.api?.scrollPrev();\n }\n\n scrollNext() {\n this.api?.scrollNext();\n }\n}\n\n@Component({\n selector: 'tolle-carousel',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n providers: [CarouselContext],\n template: `\n <div\n class=\"relative\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabindex=\"0\"\n (keydown)=\"onKeydown($event)\"\n [class.flex-col]=\"orientation === 'vertical'\"\n >\n <ng-content></ng-content>\n </div>\n `,\n host: {\n '[class.flex]': 'orientation === \"vertical\"',\n }\n})\nexport class CarouselComponent implements AfterViewInit, OnDestroy {\n @Input() opts: EmblaOptionsType = {};\n @Input() plugins: EmblaPluginType[] = [];\n @Input() orientation: 'horizontal' | 'vertical' = 'horizontal';\n\n @Output() api = new EventEmitter<EmblaCarouselType>();\n\n private carouselContext = inject(CarouselContext);\n private emblaApi?: EmblaCarouselType;\n\n @ViewChild('container', { static: false }) containerRef?: ElementRef;\n\n constructor() { }\n\n ngOnInit() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngOnChanges() {\n this.carouselContext.orientation = this.orientation;\n }\n\n ngAfterViewInit() {\n // We expect tolleCarouselContent to provide the viewport element\n }\n\n onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n this.carouselContext.scrollPrev();\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n this.carouselContext.scrollNext();\n }\n }\n\n initApi(viewport: HTMLElement) {\n this.emblaApi = EmblaCarousel(viewport, {\n ...this.opts,\n axis: this.orientation === 'horizontal' ? 'x' : 'y',\n }, this.plugins);\n\n this.carouselContext.setApi(this.emblaApi);\n this.api.emit(this.emblaApi);\n }\n\n ngOnDestroy() {\n this.emblaApi?.destroy();\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContent]',\n standalone: true,\n host: {\n 'class': 'overflow-hidden',\n }\n})\nexport class CarouselContentDirective implements AfterViewInit {\n private carousel = inject(CarouselComponent);\n private el = inject(ElementRef);\n\n ngAfterViewInit() {\n this.carousel.initApi(this.el.nativeElement);\n }\n}\n\n@Directive({\n selector: '[tolleCarouselContainer]',\n standalone: true,\n host: {\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"flex -ml-4\" : \"flex flex-col -mt-4\"',\n }\n})\nexport class CarouselContainerDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselItem]',\n standalone: true,\n host: {\n 'role': 'group',\n 'aria-roledescription': 'slide',\n '[class]': 'carouselContext.orientation === \"horizontal\" ? \"min-w-0 shrink-0 grow-0 basis-full pl-4\" : \"min-w-0 shrink-0 grow-0 basis-full pt-4\"',\n }\n})\nexport class CarouselItemDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselPrevious]',\n standalone: true,\n host: {\n 'aria-label': 'Previous slide',\n '[attr.disabled]': '!carouselContext.canScrollPrev ? true : null',\n '(click)': 'carouselContext.scrollPrev()',\n }\n})\nexport class CarouselPreviousDirective {\n carouselContext = inject(CarouselContext);\n}\n\n@Directive({\n selector: '[tolleCarouselNext]',\n standalone: true,\n host: {\n 'aria-label': 'Next slide',\n '[attr.disabled]': '!carouselContext.canScrollNext ? true : null',\n '(click)': 'carouselContext.scrollNext()',\n }\n})\nexport class CarouselNextDirective {\n carouselContext = inject(CarouselContext);\n}\n"
16
16
  }
17
17
  ]
18
18
  }